From c5279c3cef5ffb51949f81944138a8809e1e9696 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Sat, 24 Jun 2017 10:35:13 -0500 Subject: [PATCH] Using ASP.NET Core Identity (#3) --- Angular-Core-IdentityServer.sln | 28 +- ClientApp/Controllers/IdentityController.cs | 26 +- ClientApp/Views/Identity/Index.cshtml | 6 +- IdentityApp/.bowerrc | 3 + IdentityApp/Config.cs | 57 +- IdentityApp/Controllers/AccountController.cs | 519 + .../Home => Controllers}/HomeController.cs | 30 +- IdentityApp/Controllers/ManageController.cs | 373 + IdentityApp/Data/ApplicationDbContext.cs | 28 + ...000000000_CreateIdentitySchema.Designer.cs | 216 + .../00000000000000_CreateIdentitySchema.cs | 219 + .../ApplicationDbContextModelSnapshot.cs | 215 + IdentityApp/IdentityApp.csproj | 29 +- .../ExternalLoginConfirmationViewModel.cs | 15 + .../ForgotPasswordViewModel.cs | 15 + .../AccountViewModels/RegisterViewModel.cs | 27 + .../ResetPasswordViewModel.cs | 27 + .../AccountViewModels/SendCodeViewModel.cs | 19 + .../AccountViewModels/VerifyCodeViewModel.cs | 25 + IdentityApp/Models/ApplicationUser.cs | 13 + .../AddPhoneNumberViewModel.cs | 16 + .../ChangePasswordViewModel.cs | 27 + .../ConfigureTwoFactorViewModel.cs | 15 + .../ManageViewModels/FactorViewModel.cs | 12 + .../Models/ManageViewModels/IndexViewModel.cs | 21 + .../ManageViewModels/ManageLoginsViewModel.cs | 16 + .../ManageViewModels/RemoveLoginViewModel.cs | 14 + .../ManageViewModels/SetPasswordViewModel.cs | 22 + .../VerifyPhoneNumberViewModel.cs | 19 + IdentityApp/Program.cs | 1 + IdentityApp/Properties/launchSettings.json | 3 +- .../Quickstart/Account/AccountController.cs | 299 - .../Quickstart/Account/AccountOptions.cs | 6 +- .../Quickstart/Account/AccountService.cs | 10 +- .../Quickstart/Account/LoginInputModel.cs | 2 +- .../Quickstart/Consent/ConsentService.cs | 12 +- .../Quickstart/Grants/GrantsController.cs | 89 - .../Quickstart/Grants/GrantsViewModel.cs | 24 - .../Quickstart/SecurityHeadersAttribute.cs | 2 +- IdentityApp/Quickstart/TestUsers.cs | 43 - IdentityApp/Services/IEmailSender.cs | 12 + IdentityApp/Services/ISmsSender.cs | 9 + IdentityApp/Services/MessageServices.cs | 31 + IdentityApp/Startup.cs | 65 +- IdentityApp/Views/Account/ConfirmEmail.cshtml | 10 + .../Account/ExternalLoginConfirmation.cshtml | 35 + .../Views/Account/ExternalLoginFailure.cshtml | 8 + .../Views/Account/ForgotPassword.cshtml | 31 + .../Account/ForgotPasswordConfirmation.cshtml | 8 + IdentityApp/Views/Account/Lockout.cshtml | 8 + IdentityApp/Views/Account/LoggedOut.cshtml | 4 +- IdentityApp/Views/Account/Login.cshtml | 160 +- IdentityApp/Views/Account/Logout.cshtml | 3 +- IdentityApp/Views/Account/Register.cshtml | 42 + .../Views/Account/ResetPassword.cshtml | 43 + .../Account/ResetPasswordConfirmation.cshtml | 8 + IdentityApp/Views/Account/SendCode.cshtml | 21 + IdentityApp/Views/Account/VerifyCode.cshtml | 38 + IdentityApp/Views/Consent/Index.cshtml | 5 +- .../Views/Consent/_ScopeListItem.cshtml | 3 +- IdentityApp/Views/Grants/Index.cshtml | 79 - IdentityApp/Views/Home/About.cshtml | 7 + IdentityApp/Views/Home/Contact.cshtml | 17 + IdentityApp/Views/Home/Index.cshtml | 66 +- .../Views/Manage/AddPhoneNumber.cshtml | 27 + .../Views/Manage/ChangePassword.cshtml | 42 + IdentityApp/Views/Manage/Index.cshtml | 71 + IdentityApp/Views/Manage/ManageLogins.cshtml | 54 + IdentityApp/Views/Manage/SetPassword.cshtml | 38 + .../Views/Manage/VerifyPhoneNumber.cshtml | 30 + IdentityApp/Views/Shared/Error.cshtml | 10 +- IdentityApp/Views/Shared/_Layout.cshtml | 90 +- IdentityApp/Views/Shared/_LoginPartial.cshtml | 25 + .../Shared/_ValidationScriptsPartial.cshtml | 18 + IdentityApp/Views/_ViewImports.cshtml | 7 +- IdentityApp/appsettings.Development.json | 10 + IdentityApp/appsettings.json | 11 + IdentityApp/bower.json | 10 + IdentityApp/bundleconfig.json | 24 + IdentityApp/wwwroot/_references.js | 7 + IdentityApp/wwwroot/css/site.css | 108 +- IdentityApp/wwwroot/css/site.less | 116 - IdentityApp/wwwroot/css/site.min.css | 2 +- IdentityApp/wwwroot/images/banner1.svg | 1 + IdentityApp/wwwroot/images/banner2.svg | 1 + IdentityApp/wwwroot/images/banner3.svg | 1 + IdentityApp/wwwroot/images/banner4.svg | 1 + IdentityApp/wwwroot/js/site.js | 1 + IdentityApp/wwwroot/js/site.min.js | 1 + IdentityApp/wwwroot/lib/bootstrap/.bower.json | 44 + IdentityApp/wwwroot/lib/bootstrap/LICENSE | 21 + .../bootstrap/dist/css/bootstrap-theme.css | 587 + .../dist/css/bootstrap-theme.css.map | 1 + .../dist/css/bootstrap-theme.min.css | 6 + .../dist/css/bootstrap-theme.min.css.map | 1 + .../lib/bootstrap/dist/css/bootstrap.css | 6760 ++++++++++++ .../lib/bootstrap/dist/css/bootstrap.css.map | 1 + .../lib/bootstrap/dist/css/bootstrap.min.css | 6 + .../bootstrap/dist/css/bootstrap.min.css.map | 1 + .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 288 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes .../lib/bootstrap/dist/js/bootstrap.js | 2363 ++++ .../lib/bootstrap/dist/js/bootstrap.min.js | 7 + .../wwwroot/lib/bootstrap/dist/js/npm.js | 13 + .../jquery-validation-unobtrusive/.bower.json | 44 + .../jquery.validate.unobtrusive.js | 416 + .../jquery.validate.unobtrusive.min.js | 5 + .../wwwroot/lib/jquery-validation/.bower.json | 40 + .../wwwroot/lib/jquery-validation/LICENSE.md | 22 + .../dist/additional-methods.js | 998 ++ .../dist/additional-methods.min.js | 4 + .../jquery-validation/dist/jquery.validate.js | 1398 +++ .../dist/jquery.validate.min.js | 4 + IdentityApp/wwwroot/lib/jquery/.bower.json | 25 + IdentityApp/wwwroot/lib/jquery/LICENSE.txt | 36 + IdentityApp/wwwroot/lib/jquery/dist/jquery.js | 9831 +++++++++++++++++ .../wwwroot/lib/jquery/dist/jquery.min.js | 4 + .../wwwroot/lib/jquery/dist/jquery.min.map | 1 + 121 files changed, 25918 insertions(+), 971 deletions(-) create mode 100644 IdentityApp/.bowerrc create mode 100644 IdentityApp/Controllers/AccountController.cs rename IdentityApp/{Quickstart/Home => Controllers}/HomeController.cs (67%) create mode 100644 IdentityApp/Controllers/ManageController.cs create mode 100644 IdentityApp/Data/ApplicationDbContext.cs create mode 100644 IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs create mode 100644 IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs create mode 100644 IdentityApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 IdentityApp/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs create mode 100644 IdentityApp/Models/AccountViewModels/ForgotPasswordViewModel.cs create mode 100644 IdentityApp/Models/AccountViewModels/RegisterViewModel.cs create mode 100644 IdentityApp/Models/AccountViewModels/ResetPasswordViewModel.cs create mode 100644 IdentityApp/Models/AccountViewModels/SendCodeViewModel.cs create mode 100644 IdentityApp/Models/AccountViewModels/VerifyCodeViewModel.cs create mode 100644 IdentityApp/Models/ApplicationUser.cs create mode 100644 IdentityApp/Models/ManageViewModels/AddPhoneNumberViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/ChangePasswordViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/FactorViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/IndexViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/ManageLoginsViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/RemoveLoginViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/SetPasswordViewModel.cs create mode 100644 IdentityApp/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs delete mode 100644 IdentityApp/Quickstart/Account/AccountController.cs delete mode 100644 IdentityApp/Quickstart/Grants/GrantsController.cs delete mode 100644 IdentityApp/Quickstart/Grants/GrantsViewModel.cs delete mode 100644 IdentityApp/Quickstart/TestUsers.cs create mode 100644 IdentityApp/Services/IEmailSender.cs create mode 100644 IdentityApp/Services/ISmsSender.cs create mode 100644 IdentityApp/Services/MessageServices.cs create mode 100644 IdentityApp/Views/Account/ConfirmEmail.cshtml create mode 100644 IdentityApp/Views/Account/ExternalLoginConfirmation.cshtml create mode 100644 IdentityApp/Views/Account/ExternalLoginFailure.cshtml create mode 100644 IdentityApp/Views/Account/ForgotPassword.cshtml create mode 100644 IdentityApp/Views/Account/ForgotPasswordConfirmation.cshtml create mode 100644 IdentityApp/Views/Account/Lockout.cshtml create mode 100644 IdentityApp/Views/Account/Register.cshtml create mode 100644 IdentityApp/Views/Account/ResetPassword.cshtml create mode 100644 IdentityApp/Views/Account/ResetPasswordConfirmation.cshtml create mode 100644 IdentityApp/Views/Account/SendCode.cshtml create mode 100644 IdentityApp/Views/Account/VerifyCode.cshtml delete mode 100644 IdentityApp/Views/Grants/Index.cshtml create mode 100644 IdentityApp/Views/Home/About.cshtml create mode 100644 IdentityApp/Views/Home/Contact.cshtml create mode 100644 IdentityApp/Views/Manage/AddPhoneNumber.cshtml create mode 100644 IdentityApp/Views/Manage/ChangePassword.cshtml create mode 100644 IdentityApp/Views/Manage/Index.cshtml create mode 100644 IdentityApp/Views/Manage/ManageLogins.cshtml create mode 100644 IdentityApp/Views/Manage/SetPassword.cshtml create mode 100644 IdentityApp/Views/Manage/VerifyPhoneNumber.cshtml create mode 100644 IdentityApp/Views/Shared/_LoginPartial.cshtml create mode 100644 IdentityApp/Views/Shared/_ValidationScriptsPartial.cshtml create mode 100644 IdentityApp/appsettings.Development.json create mode 100644 IdentityApp/appsettings.json create mode 100644 IdentityApp/bower.json create mode 100644 IdentityApp/bundleconfig.json create mode 100644 IdentityApp/wwwroot/_references.js delete mode 100644 IdentityApp/wwwroot/css/site.less create mode 100644 IdentityApp/wwwroot/images/banner1.svg create mode 100644 IdentityApp/wwwroot/images/banner2.svg create mode 100644 IdentityApp/wwwroot/images/banner3.svg create mode 100644 IdentityApp/wwwroot/images/banner4.svg create mode 100644 IdentityApp/wwwroot/js/site.js create mode 100644 IdentityApp/wwwroot/js/site.min.js create mode 100644 IdentityApp/wwwroot/lib/bootstrap/.bower.json create mode 100644 IdentityApp/wwwroot/lib/bootstrap/LICENSE create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.css.map create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap-theme.min.css.map create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.svg create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/js/bootstrap.js create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js create mode 100644 IdentityApp/wwwroot/lib/bootstrap/dist/js/npm.js create mode 100644 IdentityApp/wwwroot/lib/jquery-validation-unobtrusive/.bower.json create mode 100644 IdentityApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js create mode 100644 IdentityApp/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js create mode 100644 IdentityApp/wwwroot/lib/jquery-validation/.bower.json create mode 100644 IdentityApp/wwwroot/lib/jquery-validation/LICENSE.md create mode 100644 IdentityApp/wwwroot/lib/jquery-validation/dist/additional-methods.js create mode 100644 IdentityApp/wwwroot/lib/jquery-validation/dist/additional-methods.min.js create mode 100644 IdentityApp/wwwroot/lib/jquery-validation/dist/jquery.validate.js create mode 100644 IdentityApp/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js create mode 100644 IdentityApp/wwwroot/lib/jquery/.bower.json create mode 100644 IdentityApp/wwwroot/lib/jquery/LICENSE.txt create mode 100644 IdentityApp/wwwroot/lib/jquery/dist/jquery.js create mode 100644 IdentityApp/wwwroot/lib/jquery/dist/jquery.min.js create mode 100644 IdentityApp/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/Angular-Core-IdentityServer.sln b/Angular-Core-IdentityServer.sln index 2265bce..f1dfee3 100644 --- a/Angular-Core-IdentityServer.sln +++ b/Angular-Core-IdentityServer.sln @@ -1,13 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.6 +VisualStudioVersion = 15.0.26430.13 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiApp", "ApiApp\ApiApp.csproj", "{454C2629-032B-4EE3-AD7E-2B08A16C264D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientApp", "ClientApp\ClientApp.csproj", "{3B48DD57-2BB6-4C48-BD05-5778F26FCE9C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityApp", "IdentityApp\IdentityApp.csproj", "{E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityApp", "IdentityApp\IdentityApp.csproj", "{4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,18 +43,18 @@ Global {3B48DD57-2BB6-4C48-BD05-5778F26FCE9C}.Release|x64.Build.0 = Release|Any CPU {3B48DD57-2BB6-4C48-BD05-5778F26FCE9C}.Release|x86.ActiveCfg = Release|Any CPU {3B48DD57-2BB6-4C48-BD05-5778F26FCE9C}.Release|x86.Build.0 = Release|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Debug|x64.ActiveCfg = Debug|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Debug|x64.Build.0 = Debug|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Debug|x86.ActiveCfg = Debug|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Debug|x86.Build.0 = Debug|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Release|Any CPU.Build.0 = Release|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Release|x64.ActiveCfg = Release|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Release|x64.Build.0 = Release|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Release|x86.ActiveCfg = Release|Any CPU - {E4C1413C-4115-4FC8-8CB2-DE91AFFBDDCC}.Release|x86.Build.0 = Release|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Debug|x64.Build.0 = Debug|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Debug|x86.Build.0 = Debug|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Release|Any CPU.Build.0 = Release|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Release|x64.ActiveCfg = Release|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Release|x64.Build.0 = Release|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Release|x86.ActiveCfg = Release|Any CPU + {4D95FBB9-C49B-4D19-86EF-9F3E67382A3D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ClientApp/Controllers/IdentityController.cs b/ClientApp/Controllers/IdentityController.cs index 423f441..d07e784 100644 --- a/ClientApp/Controllers/IdentityController.cs +++ b/ClientApp/Controllers/IdentityController.cs @@ -1,5 +1,6 @@ using System.Net.Http; using System.Threading.Tasks; +using IdentityModel.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -10,16 +11,35 @@ public class IdentityController : Controller { [Authorize] public async Task Index() + { + var apiCallUsingUserAccessToken = await ApiCallUsingUserAccessToken(); + ViewData["apiCallUsingUserAccessToken"] = apiCallUsingUserAccessToken.IsSuccessStatusCode ? await apiCallUsingUserAccessToken.Content.ReadAsStringAsync() : apiCallUsingUserAccessToken.StatusCode.ToString(); + + var clientCredentialsResponse = await ApiCallUsingClientCredentials(); + ViewData["clientCredentialsResponse"] = clientCredentialsResponse.IsSuccessStatusCode ? await clientCredentialsResponse.Content.ReadAsStringAsync() : clientCredentialsResponse.StatusCode.ToString(); + + return View(); + } + + private async Task ApiCallUsingUserAccessToken() { var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token"); var client = new HttpClient(); client.SetBearerToken(accessToken); - var apiResponse = await client.GetAsync("http://localhost:5001/api/identity"); - ViewData["apiResult"] = apiResponse.IsSuccessStatusCode ? await apiResponse.Content.ReadAsStringAsync() : apiResponse.StatusCode.ToString(); + return await client.GetAsync("http://localhost:5001/api/identity"); + } - return View(); + private async Task ApiCallUsingClientCredentials() + { + var tokenClient = new TokenClient("http://localhost:5000/connect/token", "mvc", "secret"); + var tokenResponse = await tokenClient.RequestClientCredentialsAsync("apiApp"); + + var client = new HttpClient(); + client.SetBearerToken(tokenResponse.AccessToken); + + return await client.GetAsync("http://localhost:5001/api/identity"); } public async Task Logout() diff --git a/ClientApp/Views/Identity/Index.cshtml b/ClientApp/Views/Identity/Index.cshtml index 9cedc78..f906b62 100644 --- a/ClientApp/Views/Identity/Index.cshtml +++ b/ClientApp/Views/Identity/Index.cshtml @@ -9,7 +9,11 @@
refresh token
@await ViewContext.HttpContext.Authentication.GetTokenAsync("refresh_token")
-@ViewData["apiResult"] +
api response called with user access token
+
@ViewData["apiCallUsingUserAccessToken"]
+ +
api response called with client credentials
+
@ViewData["clientCredentialsResponse"]

User claims

diff --git a/IdentityApp/.bowerrc b/IdentityApp/.bowerrc new file mode 100644 index 0000000..6406626 --- /dev/null +++ b/IdentityApp/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "wwwroot/lib" +} diff --git a/IdentityApp/Config.cs b/IdentityApp/Config.cs index 287165b..57bb628 100644 --- a/IdentityApp/Config.cs +++ b/IdentityApp/Config.cs @@ -2,22 +2,33 @@ using System.Security.Claims; using IdentityServer4; using IdentityServer4.Models; -using IdentityServer4.Test; namespace IdentityApp { public class Config { + // scopes define the resources in your system + public static IEnumerable GetIdentityResources() + { + return new List + { + new IdentityResources.OpenId(), + new IdentityResources.Profile(), + }; + } + public static IEnumerable GetApiResources() { return new List { - new ApiResource("apiApp", "API Application") + new ApiResource("apiApp", "My API") }; } + // clients want to access resources (aka scopes) public static IEnumerable GetClients() { + // client credentials client return new List { new Client @@ -44,6 +55,8 @@ public static IEnumerable GetClients() ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, + RequireConsent = true, + ClientSecrets = { new Secret("secret".Sha256()) @@ -62,45 +75,5 @@ public static IEnumerable GetClients() } }; } - - public static IEnumerable GetIdentityResources() - { - return new List - { - new IdentityResources.OpenId(), - new IdentityResources.Profile(), - }; - } - - public static List GetUsers() - { - return new List - { - new TestUser - { - SubjectId = "1", - Username = "alice", - Password = "password", - - Claims = new List - { - new Claim("name", "Alice"), - new Claim("website", "https://alice.com") - } - }, - new TestUser - { - SubjectId = "2", - Username = "bob", - Password = "password", - - Claims = new List - { - new Claim("name", "Bob"), - new Claim("website", "https://bob.com") - } - } - }; - } } } \ No newline at end of file diff --git a/IdentityApp/Controllers/AccountController.cs b/IdentityApp/Controllers/AccountController.cs new file mode 100644 index 0000000..9ea7d5e --- /dev/null +++ b/IdentityApp/Controllers/AccountController.cs @@ -0,0 +1,519 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Logging; +using IdentityApp.Models; +using IdentityApp.Models.AccountViewModels; +using IdentityApp.Services; +using IdentityServer4.Services; +using IdentityServer4.Stores; +using Microsoft.AspNetCore.Http.Authentication; +using IdentityServer4.Quickstart.UI; +using Microsoft.AspNetCore.Http; + +namespace IdentityApp.Controllers +{ + [Authorize] + [SecurityHeaders] + public class AccountController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ISmsSender _smsSender; + private readonly ILogger _logger; + private readonly AccountService _account; + + public AccountController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ISmsSender smsSender, + ILoggerFactory loggerFactory, + IIdentityServerInteractionService interaction, + IHttpContextAccessor httpContext, + IClientStore clientStore) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _smsSender = smsSender; + _logger = loggerFactory.CreateLogger(); + + _account = new AccountService(interaction, httpContext, clientStore); + } + + // + // GET: /Account/Login + [AllowAnonymous] + [HttpGet] + public async Task Login(string returnUrl) + { + var vm = await _account.BuildLoginViewModelAsync(returnUrl); + + if (vm.IsExternalLoginOnly) + { + // only one option for logging in + return ExternalLogin(vm.ExternalProviders.First().AuthenticationScheme, returnUrl); + } + + return View(vm); + } + + // + // POST: /Account/Login + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Login(LoginInputModel model) + { + 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.Email, model.Password, model.RememberLogin, lockoutOnFailure: false); + if (result.Succeeded) + { + _logger.LogInformation(1, "User logged in."); + return RedirectToLocal(model.ReturnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(SendCode), new { ReturnUrl = model.ReturnUrl, RememberMe = model.RememberLogin }); + } + if (result.IsLockedOut) + { + _logger.LogWarning(2, "User account locked out."); + return View("Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(await _account.BuildLoginViewModelAsync(model)); + } + } + + // If we got this far, something failed, redisplay form + return View(await _account.BuildLoginViewModelAsync(model)); + } + + // + // GET: /Account/Register + [HttpGet] + [AllowAnonymous] + public IActionResult Register(string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + return View(); + } + + // + // POST: /Account/Register + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model, string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + if (ModelState.IsValid) + { + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + //var callbackUrl = Url.Action(nameof(ConfirmEmail), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); + //await _emailSender.SendEmailAsync(model.Email, "Confirm your account", + // $"Please confirm your account by clicking this link: link"); + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(3, "User created a new account with password."); + return RedirectToLocal(returnUrl); + } + AddErrors(result); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + /// + /// Show logout page + /// + [AllowAnonymous] + [HttpGet] + public async Task Logout(string logoutId) + { + var vm = await _account.BuildLogoutViewModelAsync(logoutId); + + if (vm.ShowLogoutPrompt == false) + { + // no need to show prompt + return await Logout(vm); + } + + return View(vm); + } + + /// + /// Handle logout page postback + /// + [HttpPost] + [ValidateAntiForgeryToken] + [AllowAnonymous] + public async Task Logout(LogoutViewModel model) + { + var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId); + if (vm.TriggerExternalSignout) + { + string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); + try + { + // hack: try/catch to handle social providers that throw + await HttpContext.Authentication.SignOutAsync(vm.ExternalAuthenticationScheme, + new AuthenticationProperties { RedirectUri = url }); + } + catch (NotSupportedException) // this is for the external providers that don't have signout + { + } + catch (InvalidOperationException) // this is for Windows/Negotiate + { + } + } + + // delete authentication cookie + await _signInManager.SignOutAsync(); + + return View("LoggedOut", vm); + } + + // + // POST: /Account/ExternalLogin + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public IActionResult ExternalLogin(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { ReturnUrl = returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return Challenge(properties, provider); + } + + // + // GET: /Account/ExternalLoginCallback + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) + { + if (remoteError != null) + { + ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}"); + return View(nameof(Login)); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return RedirectToAction(nameof(Login)); + } + + // 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); + if (result.Succeeded) + { + _logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider); + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl }); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ViewData["ReturnUrl"] = returnUrl; + ViewData["LoginProvider"] = info.LoginProvider; + var email = info.Principal.FindFirstValue(ClaimTypes.Email); + return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email }); + } + } + + // + // POST: /Account/ExternalLoginConfirmation + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null) + { + if (ModelState.IsValid) + { + // Get the information about the user from the external login provider + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return View("ExternalLoginFailure"); + } + var user = new ApplicationUser { UserName = model.Email, Email = model.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(6, "User created an account using {Name} provider.", info.LoginProvider); + return RedirectToLocal(returnUrl); + } + } + AddErrors(result); + } + + ViewData["ReturnUrl"] = returnUrl; + return View(model); + } + + // GET: /Account/ConfirmEmail + [HttpGet] + [AllowAnonymous] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return View("Error"); + } + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return View("Error"); + } + var result = await _userManager.ConfirmEmailAsync(user, code); + return View(result.Succeeded ? "ConfirmEmail" : "Error"); + } + + // + // GET: /Account/ForgotPassword + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPassword() + { + return View(); + } + + // + // POST: /Account/ForgotPassword + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ForgotPassword(ForgotPasswordViewModel model) + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(model.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + return View("ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + //var code = await _userManager.GeneratePasswordResetTokenAsync(user); + //var callbackUrl = Url.Action(nameof(ResetPassword), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); + //await _emailSender.SendEmailAsync(model.Email, "Reset Password", + // $"Please reset your password by clicking here: link"); + //return View("ForgotPasswordConfirmation"); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/ForgotPasswordConfirmation + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/ResetPassword + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPassword(string code = null) + { + return code == null ? View("Error") : View(); + } + + // + // POST: /Account/ResetPassword + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ResetPassword(ResetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await _userManager.FindByEmailAsync(model.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account"); + } + var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); + if (result.Succeeded) + { + return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account"); + } + AddErrors(result); + return View(); + } + + // + // GET: /Account/ResetPasswordConfirmation + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/SendCode + [HttpGet] + [AllowAnonymous] + public async Task SendCode(string returnUrl = null, bool rememberMe = false) + { + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); + var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList(); + return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/SendCode + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task SendCode(SendCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + + // Generate the token and send it + var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider); + if (string.IsNullOrWhiteSpace(code)) + { + return View("Error"); + } + + var message = "Your security code is: " + code; + if (model.SelectedProvider == "Email") + { + await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message); + } + else if (model.SelectedProvider == "Phone") + { + await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message); + } + + return RedirectToAction(nameof(VerifyCode), new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); + } + + // + // GET: /Account/VerifyCode + [HttpGet] + [AllowAnonymous] + public async Task VerifyCode(string provider, bool rememberMe, string returnUrl = null) + { + // Require that the user has already logged in via username/password or external login + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/VerifyCode + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task VerifyCode(VerifyCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + // The following code protects for brute force attacks against the two factor codes. + // If a user enters incorrect codes for a specified amount of time then the user account + // will be locked out for a specified amount of time. + var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); + if (result.Succeeded) + { + return RedirectToLocal(model.ReturnUrl); + } + if (result.IsLockedOut) + { + _logger.LogWarning(7, "User account locked out."); + return View("Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid code."); + return View(model); + } + } + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + private IActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + } + + #endregion + } +} diff --git a/IdentityApp/Quickstart/Home/HomeController.cs b/IdentityApp/Controllers/HomeController.cs similarity index 67% rename from IdentityApp/Quickstart/Home/HomeController.cs rename to IdentityApp/Controllers/HomeController.cs index 80beb21..5c50d07 100644 --- a/IdentityApp/Quickstart/Home/HomeController.cs +++ b/IdentityApp/Controllers/HomeController.cs @@ -1,12 +1,9 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - - -using IdentityServer4.Services; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; +using IdentityServer4.Services; +using IdentityServer4.Quickstart.UI; -namespace IdentityServer4.Quickstart.UI +namespace IdentityApp.Controllers { [SecurityHeaders] public class HomeController : Controller @@ -23,9 +20,20 @@ public IActionResult Index() return View(); } - /// - /// Shows the error page - /// + public IActionResult About() + { + ViewData["Message"] = "Your application description page."; + + return View(); + } + + public IActionResult Contact() + { + ViewData["Message"] = "Your contact page."; + + return View(); + } + public async Task Error(string errorId) { var vm = new ErrorViewModel(); @@ -40,4 +48,4 @@ public async Task Error(string errorId) return View("Error", vm); } } -} \ No newline at end of file +} diff --git a/IdentityApp/Controllers/ManageController.cs b/IdentityApp/Controllers/ManageController.cs new file mode 100644 index 0000000..d76eea9 --- /dev/null +++ b/IdentityApp/Controllers/ManageController.cs @@ -0,0 +1,373 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using IdentityApp.Models; +using IdentityApp.Models.ManageViewModels; +using IdentityApp.Services; +using IdentityServer4.Quickstart.UI; +using Microsoft.Extensions.Options; + +namespace IdentityApp.Controllers +{ + [Authorize] + [SecurityHeaders] + public class ManageController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly string _externalCookieScheme; + private readonly IEmailSender _emailSender; + private readonly ISmsSender _smsSender; + private readonly ILogger _logger; + + public ManageController( + UserManager userManager, + SignInManager signInManager, + IOptions identityCookieOptions, + IEmailSender emailSender, + ISmsSender smsSender, + ILoggerFactory loggerFactory) + { + _userManager = userManager; + _signInManager = signInManager; + _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme; + _emailSender = emailSender; + _smsSender = smsSender; + _logger = loggerFactory.CreateLogger(); + } + + // + // GET: /Manage/Index + [HttpGet] + public async Task Index(ManageMessageId? message = null) + { + ViewData["StatusMessage"] = + message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." + : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." + : message == ManageMessageId.SetTwoFactorSuccess ? "Your two-factor authentication provider has been set." + : message == ManageMessageId.Error ? "An error has occurred." + : message == ManageMessageId.AddPhoneSuccess ? "Your phone number was added." + : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed." + : ""; + + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var model = new IndexViewModel + { + HasPassword = await _userManager.HasPasswordAsync(user), + PhoneNumber = await _userManager.GetPhoneNumberAsync(user), + TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user), + Logins = await _userManager.GetLoginsAsync(user), + BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user) + }; + return View(model); + } + + // + // POST: /Manage/RemoveLogin + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RemoveLogin(RemoveLoginViewModel account) + { + ManageMessageId? message = ManageMessageId.Error; + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.RemoveLoginAsync(user, account.LoginProvider, account.ProviderKey); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + message = ManageMessageId.RemoveLoginSuccess; + } + } + return RedirectToAction(nameof(ManageLogins), new { Message = message }); + } + + // + // GET: /Manage/AddPhoneNumber + public IActionResult AddPhoneNumber() + { + return View(); + } + + // + // POST: /Manage/AddPhoneNumber + [HttpPost] + [ValidateAntiForgeryToken] + public async Task AddPhoneNumber(AddPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + // Generate the token and send it + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, model.PhoneNumber); + await _smsSender.SendSmsAsync(model.PhoneNumber, "Your security code is: " + code); + return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber }); + } + + // + // POST: /Manage/EnableTwoFactorAuthentication + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EnableTwoFactorAuthentication() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await _userManager.SetTwoFactorEnabledAsync(user, true); + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(1, "User enabled two-factor authentication."); + } + return RedirectToAction(nameof(Index), "Manage"); + } + + // + // POST: /Manage/DisableTwoFactorAuthentication + [HttpPost] + [ValidateAntiForgeryToken] + public async Task DisableTwoFactorAuthentication() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(2, "User disabled two-factor authentication."); + } + return RedirectToAction(nameof(Index), "Manage"); + } + + // + // GET: /Manage/VerifyPhoneNumber + [HttpGet] + public async Task VerifyPhoneNumber(string phoneNumber) + { + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, phoneNumber); + // Send an SMS to verify the phone number + return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); + } + + // + // POST: /Manage/VerifyPhoneNumber + [HttpPost] + [ValidateAntiForgeryToken] + public async Task VerifyPhoneNumber(VerifyPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.AddPhoneSuccess }); + } + } + // If we got this far, something failed, redisplay the form + ModelState.AddModelError(string.Empty, "Failed to verify phone number"); + return View(model); + } + + // + // POST: /Manage/RemovePhoneNumber + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RemovePhoneNumber() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.SetPhoneNumberAsync(user, null); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.RemovePhoneSuccess }); + } + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/ChangePassword + [HttpGet] + public IActionResult ChangePassword() + { + return View(); + } + + // + // POST: /Manage/ChangePassword + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ChangePassword(ChangePasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(3, "User changed their password successfully."); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangePasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/SetPassword + [HttpGet] + public IActionResult SetPassword() + { + return View(); + } + + // + // POST: /Manage/SetPassword + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SetPassword(SetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.AddPasswordAsync(user, model.NewPassword); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetPasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + //GET: /Manage/ManageLogins + [HttpGet] + public async Task ManageLogins(ManageMessageId? message = null) + { + ViewData["StatusMessage"] = + message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." + : message == ManageMessageId.AddLoginSuccess ? "The external login was added." + : message == ManageMessageId.Error ? "An error has occurred." + : ""; + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var userLogins = await _userManager.GetLoginsAsync(user); + var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList(); + ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1; + return View(new ManageLoginsViewModel + { + CurrentLogins = userLogins, + OtherLogins = otherLogins + }); + } + + // + // POST: /Manage/LinkLogin + [HttpPost] + [ValidateAntiForgeryToken] + public async Task LinkLogin(string provider) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.Authentication.SignOutAsync(_externalCookieScheme); + + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Action(nameof(LinkLoginCallback), "Manage"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return Challenge(properties, provider); + } + + // + // GET: /Manage/LinkLoginCallback + [HttpGet] + public async Task LinkLoginCallback() + { + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); + if (info == null) + { + return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error }); + } + var result = await _userManager.AddLoginAsync(user, info); + var message = ManageMessageId.Error; + if (result.Succeeded) + { + message = ManageMessageId.AddLoginSuccess; + // Clear the existing external cookie to ensure a clean login process + await HttpContext.Authentication.SignOutAsync(_externalCookieScheme); + } + return RedirectToAction(nameof(ManageLogins), new { Message = message }); + } + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + public enum ManageMessageId + { + AddPhoneSuccess, + AddLoginSuccess, + ChangePasswordSuccess, + SetTwoFactorSuccess, + SetPasswordSuccess, + RemoveLoginSuccess, + RemovePhoneSuccess, + Error + } + + private Task GetCurrentUserAsync() + { + return _userManager.GetUserAsync(HttpContext.User); + } + + #endregion + } +} diff --git a/IdentityApp/Data/ApplicationDbContext.cs b/IdentityApp/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..5b9cc13 --- /dev/null +++ b/IdentityApp/Data/ApplicationDbContext.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using IdentityApp.Models; + +namespace IdentityApp.Data +{ + public sealed class ApplicationDbContext : IdentityDbContext + { + private static bool _migrated; + + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + if (_migrated) return; + Database.Migrate(); + _migrated = true; + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + } + } +} diff --git a/IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..bfc55b6 --- /dev/null +++ b/IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace IdentityApp.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("IdentityApp.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasAnnotation("MaxLength", 256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedUserName") + .HasAnnotation("MaxLength", 256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.HasOne("IdentityApp.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.HasOne("IdentityApp.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IdentityApp.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000..01d65ad --- /dev/null +++ b/IdentityApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace IdentityApp.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + LockoutEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + PasswordHash = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + SecurityStamp = table.Column(nullable: true), + TwoFactorEnabled = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_UserId", + table: "AspNetUserRoles", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/IdentityApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/IdentityApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..072272e --- /dev/null +++ b/IdentityApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace IdentityApp.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("IdentityApp.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasAnnotation("MaxLength", 256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedUserName") + .HasAnnotation("MaxLength", 256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.HasOne("IdentityApp.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.HasOne("IdentityApp.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IdentityApp.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/IdentityApp/IdentityApp.csproj b/IdentityApp/IdentityApp.csproj index 8dc1ceb..fc51d53 100644 --- a/IdentityApp/IdentityApp.csproj +++ b/IdentityApp/IdentityApp.csproj @@ -4,14 +4,35 @@ netcoreapp1.1 + + $(PackageTargetFallback);portable-net45+win8+wp8+wpa81; + + + + aspnet-IdentityApp-77a69134-7ece-4481-86db-b56de2a12985 + - - - - + + + + + + + + + + + + + + + + + + diff --git a/IdentityApp/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs b/IdentityApp/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs new file mode 100644 index 0000000..76ec801 --- /dev/null +++ b/IdentityApp/Models/AccountViewModels/ExternalLoginConfirmationViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.AccountViewModels +{ + public class ExternalLoginConfirmationViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } +} diff --git a/IdentityApp/Models/AccountViewModels/ForgotPasswordViewModel.cs b/IdentityApp/Models/AccountViewModels/ForgotPasswordViewModel.cs new file mode 100644 index 0000000..a24acb1 --- /dev/null +++ b/IdentityApp/Models/AccountViewModels/ForgotPasswordViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.AccountViewModels +{ + public class ForgotPasswordViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } +} diff --git a/IdentityApp/Models/AccountViewModels/RegisterViewModel.cs b/IdentityApp/Models/AccountViewModels/RegisterViewModel.cs new file mode 100644 index 0000000..2eeb214 --- /dev/null +++ b/IdentityApp/Models/AccountViewModels/RegisterViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.AccountViewModels +{ + public class RegisterViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} diff --git a/IdentityApp/Models/AccountViewModels/ResetPasswordViewModel.cs b/IdentityApp/Models/AccountViewModels/ResetPasswordViewModel.cs new file mode 100644 index 0000000..a69f704 --- /dev/null +++ b/IdentityApp/Models/AccountViewModels/ResetPasswordViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.AccountViewModels +{ + public class ResetPasswordViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } +} diff --git a/IdentityApp/Models/AccountViewModels/SendCodeViewModel.cs b/IdentityApp/Models/AccountViewModels/SendCodeViewModel.cs new file mode 100644 index 0000000..ec4144b --- /dev/null +++ b/IdentityApp/Models/AccountViewModels/SendCodeViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace IdentityApp.Models.AccountViewModels +{ + public class SendCodeViewModel + { + public string SelectedProvider { get; set; } + + public ICollection Providers { get; set; } + + public string ReturnUrl { get; set; } + + public bool RememberMe { get; set; } + } +} diff --git a/IdentityApp/Models/AccountViewModels/VerifyCodeViewModel.cs b/IdentityApp/Models/AccountViewModels/VerifyCodeViewModel.cs new file mode 100644 index 0000000..1599057 --- /dev/null +++ b/IdentityApp/Models/AccountViewModels/VerifyCodeViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.AccountViewModels +{ + public class VerifyCodeViewModel + { + [Required] + public string Provider { get; set; } + + [Required] + public string Code { get; set; } + + public string ReturnUrl { get; set; } + + [Display(Name = "Remember this browser?")] + public bool RememberBrowser { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} diff --git a/IdentityApp/Models/ApplicationUser.cs b/IdentityApp/Models/ApplicationUser.cs new file mode 100644 index 0000000..4b757dc --- /dev/null +++ b/IdentityApp/Models/ApplicationUser.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; + +namespace IdentityApp.Models +{ + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + } +} diff --git a/IdentityApp/Models/ManageViewModels/AddPhoneNumberViewModel.cs b/IdentityApp/Models/ManageViewModels/AddPhoneNumberViewModel.cs new file mode 100644 index 0000000..0556fee --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/AddPhoneNumberViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.ManageViewModels +{ + public class AddPhoneNumberViewModel + { + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/ChangePasswordViewModel.cs b/IdentityApp/Models/ManageViewModels/ChangePasswordViewModel.cs new file mode 100644 index 0000000..43f6b50 --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/ChangePasswordViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.ManageViewModels +{ + public class ChangePasswordViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs b/IdentityApp/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs new file mode 100644 index 0000000..a34f660 --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace IdentityApp.Models.ManageViewModels +{ + public class ConfigureTwoFactorViewModel + { + public string SelectedProvider { get; set; } + + public ICollection Providers { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/FactorViewModel.cs b/IdentityApp/Models/ManageViewModels/FactorViewModel.cs new file mode 100644 index 0000000..ea7c7f7 --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/FactorViewModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.ManageViewModels +{ + public class FactorViewModel + { + public string Purpose { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/IndexViewModel.cs b/IdentityApp/Models/ManageViewModels/IndexViewModel.cs new file mode 100644 index 0000000..1e81863 --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/IndexViewModel.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace IdentityApp.Models.ManageViewModels +{ + public class IndexViewModel + { + public bool HasPassword { get; set; } + + public IList Logins { get; set; } + + public string PhoneNumber { get; set; } + + public bool TwoFactor { get; set; } + + public bool BrowserRemembered { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/ManageLoginsViewModel.cs b/IdentityApp/Models/ManageViewModels/ManageLoginsViewModel.cs new file mode 100644 index 0000000..e84d845 --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/ManageLoginsViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Identity; + +namespace IdentityApp.Models.ManageViewModels +{ + public class ManageLoginsViewModel + { + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/RemoveLoginViewModel.cs b/IdentityApp/Models/ManageViewModels/RemoveLoginViewModel.cs new file mode 100644 index 0000000..47b22a8 --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/RemoveLoginViewModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.ManageViewModels +{ + public class RemoveLoginViewModel + { + public string LoginProvider { get; set; } + public string ProviderKey { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/SetPasswordViewModel.cs b/IdentityApp/Models/ManageViewModels/SetPasswordViewModel.cs new file mode 100644 index 0000000..54a5268 --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/SetPasswordViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.ManageViewModels +{ + public class SetPasswordViewModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} diff --git a/IdentityApp/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs b/IdentityApp/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs new file mode 100644 index 0000000..e179a9c --- /dev/null +++ b/IdentityApp/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Models.ManageViewModels +{ + public class VerifyPhoneNumberViewModel + { + [Required] + public string Code { get; set; } + + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } +} diff --git a/IdentityApp/Program.cs b/IdentityApp/Program.cs index 78330e0..6cb8d7f 100644 --- a/IdentityApp/Program.cs +++ b/IdentityApp/Program.cs @@ -16,6 +16,7 @@ public static void Main(string[] args) .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() + .UseApplicationInsights() .Build(); host.Run(); diff --git a/IdentityApp/Properties/launchSettings.json b/IdentityApp/Properties/launchSettings.json index dea9211..0e7de38 100644 --- a/IdentityApp/Properties/launchSettings.json +++ b/IdentityApp/Properties/launchSettings.json @@ -3,13 +3,14 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:5000/", + "applicationUrl": "http://localhost:5000", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", + "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/IdentityApp/Quickstart/Account/AccountController.cs b/IdentityApp/Quickstart/Account/AccountController.cs deleted file mode 100644 index 0a07eb8..0000000 --- a/IdentityApp/Quickstart/Account/AccountController.cs +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - - -using IdentityModel; -using IdentityServer4.Services; -using IdentityServer4.Stores; -using IdentityServer4.Test; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Security.Principal; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using IdentityServer4.Events; -using IdentityServer4.Extensions; - -namespace IdentityServer4.Quickstart.UI -{ - /// - /// This sample controller implements a typical login/logout/provision workflow for local and external accounts. - /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production! - /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval - /// - [SecurityHeaders] - public class AccountController : Controller - { - private readonly TestUserStore _users; - private readonly IIdentityServerInteractionService _interaction; - private readonly IEventService _events; - private readonly AccountService _account; - - public AccountController( - IIdentityServerInteractionService interaction, - IClientStore clientStore, - IHttpContextAccessor httpContextAccessor, - IEventService events, - TestUserStore users = null) - { - // if the TestUserStore is not in DI, then we'll just use the global users collection - _users = users ?? new TestUserStore(TestUsers.Users); - _interaction = interaction; - _events = events; - _account = new AccountService(interaction, httpContextAccessor, clientStore); - } - - /// - /// Show login page - /// - [HttpGet] - public async Task Login(string returnUrl) - { - var vm = await _account.BuildLoginViewModelAsync(returnUrl); - - if (vm.IsExternalLoginOnly) - { - // only one option for logging in - return await ExternalLogin(vm.ExternalLoginScheme, returnUrl); - } - - return View(vm); - } - - /// - /// Handle postback from username/password login - /// - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Login(LoginInputModel model) - { - if (ModelState.IsValid) - { - // validate username/password against in-memory store - if (_users.ValidateCredentials(model.Username, model.Password)) - { - AuthenticationProperties props = null; - // only set explicit expiration here if persistent. - // otherwise we reply upon expiration configured in cookie middleware. - if (AccountOptions.AllowRememberLogin && model.RememberLogin) - { - props = new AuthenticationProperties - { - IsPersistent = true, - ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) - }; - }; - - // issue authentication cookie with subject ID and username - var user = _users.FindByUsername(model.Username); - await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); - await HttpContext.Authentication.SignInAsync(user.SubjectId, user.Username, props); - - // make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint or a local page - if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) - { - return Redirect(model.ReturnUrl); - } - - return Redirect("~/"); - } - - await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); - - ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); - } - - // something went wrong, show form with error - var vm = await _account.BuildLoginViewModelAsync(model); - return View(vm); - } - - /// - /// initiate roundtrip to external authentication provider - /// - [HttpGet] - public async Task ExternalLogin(string provider, string returnUrl) - { - returnUrl = Url.Action("ExternalLoginCallback", new { returnUrl = returnUrl }); - - // windows authentication is modeled as external in the asp.net core authentication manager, so we need special handling - if (AccountOptions.WindowsAuthenticationSchemes.Contains(provider)) - { - // but they don't support the redirect uri, so this URL is re-triggered when we call challenge - if (HttpContext.User is WindowsPrincipal wp) - { - var props = new AuthenticationProperties(); - props.Items.Add("scheme", AccountOptions.WindowsAuthenticationProviderName); - - var id = new ClaimsIdentity(provider); - id.AddClaim(new Claim(JwtClaimTypes.Subject, HttpContext.User.Identity.Name)); - id.AddClaim(new Claim(JwtClaimTypes.Name, HttpContext.User.Identity.Name)); - - // add the groups as claims -- be careful if the number of groups is too large - if (AccountOptions.IncludeWindowsGroups) - { - var wi = wp.Identity as WindowsIdentity; - var groups = wi.Groups.Translate(typeof(NTAccount)); - var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value)); - id.AddClaims(roles); - } - - await HttpContext.Authentication.SignInAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme, new ClaimsPrincipal(id), props); - return Redirect(returnUrl); - } - else - { - // this triggers all of the windows auth schemes we're supporting so the browser can use what it supports - return new ChallengeResult(AccountOptions.WindowsAuthenticationSchemes); - } - } - else - { - // start challenge and roundtrip the return URL - var props = new AuthenticationProperties - { - RedirectUri = returnUrl, - Items = { { "scheme", provider } } - }; - return new ChallengeResult(provider, props); - } - } - - /// - /// Post processing of external authentication - /// - [HttpGet] - public async Task ExternalLoginCallback(string returnUrl) - { - // read external identity from the temporary cookie - var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); - var tempUser = info?.Principal; - if (tempUser == null) - { - throw new Exception("External authentication error"); - } - - // retrieve claims of the external user - var claims = tempUser.Claims.ToList(); - - // try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier - // depending on the external provider, some other claim type might be used - var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject); - if (userIdClaim == null) - { - userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier); - } - if (userIdClaim == null) - { - throw new Exception("Unknown userid"); - } - - // remove the user id claim from the claims collection and move to the userId property - // also set the name of the external authentication provider - claims.Remove(userIdClaim); - var provider = info.Properties.Items["scheme"]; - var userId = userIdClaim.Value; - - // check if the external user is already provisioned - var user = _users.FindByExternalProvider(provider, userId); - if (user == null) - { - // this sample simply auto-provisions new external user - // another common approach is to start a registrations workflow first - user = _users.AutoProvisionUser(provider, userId, claims); - } - - var additionalClaims = new List(); - - // if the external system sent a session id claim, copy it over - var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); - if (sid != null) - { - additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); - } - - // if the external provider issued an id_token, we'll keep it for signout - AuthenticationProperties props = null; - var id_token = info.Properties.GetTokenValue("id_token"); - if (id_token != null) - { - props = new AuthenticationProperties(); - props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); - } - - // issue authentication cookie for user - await _events.RaiseAsync(new UserLoginSuccessEvent(provider, userId, user.SubjectId, user.Username)); - await HttpContext.Authentication.SignInAsync(user.SubjectId, user.Username, provider, props, additionalClaims.ToArray()); - - // delete temporary cookie used during external authentication - await HttpContext.Authentication.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); - - // validate return URL and redirect back to authorization endpoint or a local page - if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl)) - { - return Redirect(returnUrl); - } - - return Redirect("~/"); - } - - /// - /// Show logout page - /// - [HttpGet] - public async Task Logout(string logoutId) - { - var vm = await _account.BuildLogoutViewModelAsync(logoutId); - - if (vm.ShowLogoutPrompt == false) - { - // no need to show prompt - return await Logout(vm); - } - - return View(vm); - } - - /// - /// Handle logout page postback - /// - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Logout(LogoutInputModel model) - { - var vm = await _account.BuildLoggedOutViewModelAsync(model.LogoutId); - if (vm.TriggerExternalSignout) - { - string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); - try - { - // hack: try/catch to handle social providers that throw - await HttpContext.Authentication.SignOutAsync(vm.ExternalAuthenticationScheme, - new AuthenticationProperties { RedirectUri = url }); - } - catch (NotSupportedException) // this is for the external providers that don't have signout - { - } - catch (InvalidOperationException) // this is for Windows/Negotiate - { - } - } - - // delete local authentication cookie - await HttpContext.Authentication.SignOutAsync(); - - var user = await HttpContext.GetIdentityServerUserAsync(); - if (user != null) - { - await _events.RaiseAsync(new UserLogoutSuccessEvent(user.GetSubjectId(), user.GetName())); - } - - return View("LoggedOut", vm); - } - } -} \ No newline at end of file diff --git a/IdentityApp/Quickstart/Account/AccountOptions.cs b/IdentityApp/Quickstart/Account/AccountOptions.cs index 101c66f..14de63a 100644 --- a/IdentityApp/Quickstart/Account/AccountOptions.cs +++ b/IdentityApp/Quickstart/Account/AccountOptions.cs @@ -15,13 +15,9 @@ public class AccountOptions public static bool ShowLogoutPrompt = true; public static bool AutomaticRedirectAfterSignOut = false; - // to enable windows authentication, the host (IIS or IIS Express) also must have - // windows auth enabled. - public static bool WindowsAuthenticationEnabled = true; - public static bool IncludeWindowsGroups = false; + public static bool WindowsAuthenticationEnabled = false; // specify the Windows authentication schemes you want to use for authentication public static readonly string[] WindowsAuthenticationSchemes = new string[] { "Negotiate", "NTLM" }; - public static readonly string WindowsAuthenticationProviderName = "Windows"; public static readonly string WindowsAuthenticationDisplayName = "Windows"; public static string InvalidCredentialsErrorMessage = "Invalid username or password"; diff --git a/IdentityApp/Quickstart/Account/AccountService.cs b/IdentityApp/Quickstart/Account/AccountService.cs index 37be7d8..33871fc 100644 --- a/IdentityApp/Quickstart/Account/AccountService.cs +++ b/IdentityApp/Quickstart/Account/AccountService.cs @@ -38,8 +38,8 @@ public async Task BuildLoginViewModelAsync(string returnUrl) { EnableLocalLogin = false, ReturnUrl = returnUrl, - Username = context?.LoginHint, - ExternalProviders = new ExternalProvider[] {new ExternalProvider { AuthenticationScheme = context.IdP } } + Email = context?.LoginHint, + ExternalProviders = new ExternalProvider[] { new ExternalProvider { AuthenticationScheme = context.IdP } } }; } @@ -87,7 +87,7 @@ public async Task BuildLoginViewModelAsync(string returnUrl) AllowRememberLogin = AccountOptions.AllowRememberLogin, EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, ReturnUrl = returnUrl, - Username = context?.LoginHint, + Email = context?.LoginHint, ExternalProviders = providers.ToArray() }; } @@ -95,7 +95,7 @@ public async Task BuildLoginViewModelAsync(string returnUrl) public async Task BuildLoginViewModelAsync(LoginInputModel model) { var vm = await BuildLoginViewModelAsync(model.ReturnUrl); - vm.Username = model.Username; + vm.Email = model.Email; vm.RememberLogin = model.RememberLogin; return vm; } @@ -143,7 +143,7 @@ public async Task BuildLoggedOutViewModelAsync(string logout if (user != null) { var idp = user.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; - if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider) + if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider) { if (vm.LogoutId == null) { diff --git a/IdentityApp/Quickstart/Account/LoginInputModel.cs b/IdentityApp/Quickstart/Account/LoginInputModel.cs index 66c9cc4..2caa8c4 100644 --- a/IdentityApp/Quickstart/Account/LoginInputModel.cs +++ b/IdentityApp/Quickstart/Account/LoginInputModel.cs @@ -9,7 +9,7 @@ namespace IdentityServer4.Quickstart.UI public class LoginInputModel { [Required] - public string Username { get; set; } + public string Email { get; set; } [Required] public string Password { get; set; } public bool RememberLogin { get; set; } diff --git a/IdentityApp/Quickstart/Consent/ConsentService.cs b/IdentityApp/Quickstart/Consent/ConsentService.cs index 0628c44..cd70ae8 100644 --- a/IdentityApp/Quickstart/Consent/ConsentService.cs +++ b/IdentityApp/Quickstart/Consent/ConsentService.cs @@ -50,7 +50,7 @@ public async Task ProcessConsent(ConsentInputModel model) var scopes = model.ScopesConsented; if (ConsentOptions.EnableOfflineAccess == false) { - scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); + scopes = scopes.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess); } grantedConsent = new ConsentResponse @@ -73,7 +73,7 @@ public async Task ProcessConsent(ConsentInputModel model) { // validate return url is still valid var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); - if (request == null) return result; + if (result == null) return result; // communicate outcome of consent back to identityserver await _interaction.GrantConsentAsync(request, grantedConsent); @@ -122,8 +122,8 @@ public async Task BuildViewModelAsync(string returnUrl, Consen } private ConsentViewModel CreateConsentViewModel( - ConsentInputModel model, string returnUrl, - AuthorizationRequest request, + ConsentInputModel model, string returnUrl, + AuthorizationRequest request, Client client, Resources resources) { var vm = new ConsentViewModel(); @@ -142,7 +142,7 @@ private ConsentViewModel CreateConsentViewModel( if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) { vm.ResourceScopes = vm.ResourceScopes.Union(new ScopeViewModel[] { - GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) + GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) }); } @@ -179,7 +179,7 @@ private ScopeViewModel GetOfflineAccessScope(bool check) { return new ScopeViewModel { - Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, + Name = IdentityServerConstants.StandardScopes.OfflineAccess, DisplayName = ConsentOptions.OfflineAccessDisplayName, Description = ConsentOptions.OfflineAccessDescription, Emphasize = true, diff --git a/IdentityApp/Quickstart/Grants/GrantsController.cs b/IdentityApp/Quickstart/Grants/GrantsController.cs deleted file mode 100644 index 4e408af..0000000 --- a/IdentityApp/Quickstart/Grants/GrantsController.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - - -using IdentityServer4.Services; -using IdentityServer4.Stores; -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; - -namespace IdentityServer4.Quickstart.UI -{ - /// - /// This sample controller allows a user to revoke grants given to clients - /// - [SecurityHeaders] - [Authorize(ActiveAuthenticationSchemes = IdentityServer4.IdentityServerConstants.DefaultCookieAuthenticationScheme)] - public class GrantsController : Controller - { - private readonly IIdentityServerInteractionService _interaction; - private readonly IClientStore _clients; - private readonly IResourceStore _resources; - - public GrantsController(IIdentityServerInteractionService interaction, - IClientStore clients, - IResourceStore resources) - { - _interaction = interaction; - _clients = clients; - _resources = resources; - } - - /// - /// Show list of grants - /// - [HttpGet] - public async Task Index() - { - return View("Index", await BuildViewModelAsync()); - } - - /// - /// Handle postback to revoke a client - /// - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Revoke(string clientId) - { - await _interaction.RevokeUserConsentAsync(clientId); - return RedirectToAction("Index"); - } - - async Task BuildViewModelAsync() - { - var grants = await _interaction.GetAllUserConsentsAsync(); - - var list = new List(); - foreach(var grant in grants) - { - var client = await _clients.FindClientByIdAsync(grant.ClientId); - if (client != null) - { - var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); - - var item = new GrantViewModel() - { - ClientId = client.ClientId, - ClientName = client.ClientName ?? client.ClientId, - ClientLogoUrl = client.LogoUri, - ClientUrl = client.ClientUri, - Created = grant.CreationTime, - Expires = grant.Expiration, - IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), - ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray(), - }; - - list.Add(item); - } - } - - return new GrantsViewModel - { - Grants = list - }; - } - } -} \ No newline at end of file diff --git a/IdentityApp/Quickstart/Grants/GrantsViewModel.cs b/IdentityApp/Quickstart/Grants/GrantsViewModel.cs deleted file mode 100644 index 14ef0f4..0000000 --- a/IdentityApp/Quickstart/Grants/GrantsViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace IdentityServer4.Quickstart.UI -{ - public class GrantsViewModel - { - public IEnumerable Grants { get; set; } - } - - public class GrantViewModel - { - public string ClientId { get; set; } - public string ClientName { get; set; } - public string ClientUrl { get; set; } - public string ClientLogoUrl { get; set; } - public DateTime Created { get; set; } - public DateTime? Expires { get; set; } - public IEnumerable IdentityGrantNames { get; set; } - public IEnumerable ApiGrantNames { get; set; } - } -} diff --git a/IdentityApp/Quickstart/SecurityHeadersAttribute.cs b/IdentityApp/Quickstart/SecurityHeadersAttribute.cs index 1d07395..1b40f96 100644 --- a/IdentityApp/Quickstart/SecurityHeadersAttribute.cs +++ b/IdentityApp/Quickstart/SecurityHeadersAttribute.cs @@ -26,7 +26,7 @@ public override void OnResultExecuting(ResultExecutingContext context) var csp = "default-src 'self';"; // an example if you need client images to be displayed from twitter //var csp = "default-src 'self'; img-src 'self' https://pbs.twimg.com"; - + // once for standards compliant browsers if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) { diff --git a/IdentityApp/Quickstart/TestUsers.cs b/IdentityApp/Quickstart/TestUsers.cs deleted file mode 100644 index f7e0bcf..0000000 --- a/IdentityApp/Quickstart/TestUsers.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - - -using IdentityModel; -using IdentityServer4.Test; -using System.Collections.Generic; -using System.Security.Claims; - -namespace IdentityServer4.Quickstart.UI -{ - public class TestUsers - { - public static List Users = new List - { - new TestUser{SubjectId = "818727", Username = "alice", Password = "alice", - Claims = - { - new Claim(JwtClaimTypes.Name, "Alice Smith"), - new Claim(JwtClaimTypes.GivenName, "Alice"), - new Claim(JwtClaimTypes.FamilyName, "Smith"), - new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), - new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), - new Claim(JwtClaimTypes.WebSite, "http://alice.com"), - new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) - } - }, - new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob", - Claims = - { - new Claim(JwtClaimTypes.Name, "Bob Smith"), - new Claim(JwtClaimTypes.GivenName, "Bob"), - new Claim(JwtClaimTypes.FamilyName, "Smith"), - new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), - new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), - new Claim(JwtClaimTypes.WebSite, "http://bob.com"), - new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), - new Claim("location", "somewhere"), - } - }, - }; - } -} \ No newline at end of file diff --git a/IdentityApp/Services/IEmailSender.cs b/IdentityApp/Services/IEmailSender.cs new file mode 100644 index 0000000..d204a7c --- /dev/null +++ b/IdentityApp/Services/IEmailSender.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IdentityApp.Services +{ + public interface IEmailSender + { + Task SendEmailAsync(string email, string subject, string message); + } +} diff --git a/IdentityApp/Services/ISmsSender.cs b/IdentityApp/Services/ISmsSender.cs new file mode 100644 index 0000000..d00a66e --- /dev/null +++ b/IdentityApp/Services/ISmsSender.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace IdentityApp.Services +{ + public interface ISmsSender + { + Task SendSmsAsync(string number, string message); + } +} diff --git a/IdentityApp/Services/MessageServices.cs b/IdentityApp/Services/MessageServices.cs new file mode 100644 index 0000000..73f4f7b --- /dev/null +++ b/IdentityApp/Services/MessageServices.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace IdentityApp.Services +{ + // This class is used by the application to send Email and SMS + // when you turn on two-factor authentication in ASP.NET Identity. + // For more details see this link http://go.microsoft.com/fwlink/?LinkID=532713 + public class AuthMessageSender : IEmailSender, ISmsSender + { + private readonly ILogger _logger; + + public AuthMessageSender(ILogger logger) + { + _logger = logger; + } + public Task SendEmailAsync(string email, string subject, string message) + { + // Plug in your email service here to send an email. + _logger.LogInformation("Email: {email}, Subject: {subject}, Message: {message}", email, subject, message); + return Task.FromResult(0); + } + + public Task SendSmsAsync(string number, string message) + { + // Plug in your SMS service here to send a text message. + _logger.LogInformation("SMS: {number}, Message: {message}", number, message); + return Task.FromResult(0); + } + } +} diff --git a/IdentityApp/Startup.cs b/IdentityApp/Startup.cs index a2b135d..8b5080e 100644 --- a/IdentityApp/Startup.cs +++ b/IdentityApp/Startup.cs @@ -4,42 +4,93 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using IdentityApp.Data; +using IdentityApp.Models; +using IdentityApp.Services; namespace IdentityApp { public class Startup { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + + if (env.IsDevelopment()) + { + // For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709 + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + // Add framework services. + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + services.AddMvc(); + // Add application services. + services.AddTransient(); + services.AddTransient(); + services.AddIdentityServer() .AddTemporarySigningCredential() + .AddInMemoryPersistedGrants() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()) - .AddTestUsers(Config.GetUsers()); + .AddAspNetIdentity(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { - loggerFactory.AddConsole(); + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); + app.UseDatabaseErrorPage(); + app.UseBrowserLink(); + } + else + { + app.UseExceptionHandler("/Home/Error"); } - app.UseIdentityServer(); - app.UseDeveloperExceptionPage(); app.UseStaticFiles(); - app.UseMvcWithDefaultRoute(); + + app.UseIdentity(); + + app.UseIdentityServer(); + + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + }); } } } diff --git a/IdentityApp/Views/Account/ConfirmEmail.cshtml b/IdentityApp/Views/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000..3244fef --- /dev/null +++ b/IdentityApp/Views/Account/ConfirmEmail.cshtml @@ -0,0 +1,10 @@ +@{ + ViewData["Title"] = "Confirm Email"; +} + +

@ViewData["Title"].

+
+

+ Thank you for confirming your email. Please Click here to Log in. +

+
diff --git a/IdentityApp/Views/Account/ExternalLoginConfirmation.cshtml b/IdentityApp/Views/Account/ExternalLoginConfirmation.cshtml new file mode 100644 index 0000000..987fff4 --- /dev/null +++ b/IdentityApp/Views/Account/ExternalLoginConfirmation.cshtml @@ -0,0 +1,35 @@ +@model ExternalLoginConfirmationViewModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"].

+

Associate your @ViewData["LoginProvider"] account.

+ +
+

Association Form

+
+
+ +

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

+
+ +
+ + +
+
+
+
+ +
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/IdentityApp/Views/Account/ExternalLoginFailure.cshtml b/IdentityApp/Views/Account/ExternalLoginFailure.cshtml new file mode 100644 index 0000000..d89339e --- /dev/null +++ b/IdentityApp/Views/Account/ExternalLoginFailure.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Login Failure"; +} + +
+

@ViewData["Title"].

+

Unsuccessful login with service.

+
diff --git a/IdentityApp/Views/Account/ForgotPassword.cshtml b/IdentityApp/Views/Account/ForgotPassword.cshtml new file mode 100644 index 0000000..4cdc71c --- /dev/null +++ b/IdentityApp/Views/Account/ForgotPassword.cshtml @@ -0,0 +1,31 @@ +@model ForgotPasswordViewModel +@{ + ViewData["Title"] = "Forgot your password?"; +} + +

@ViewData["Title"]

+

+ For more information on how to enable reset password please see this article. +

+ +@*
+

Enter your email.

+
+
+
+ +
+ + +
+
+
+
+ +
+
+
*@ + +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/IdentityApp/Views/Account/ForgotPasswordConfirmation.cshtml b/IdentityApp/Views/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000..ab9bf44 --- /dev/null +++ b/IdentityApp/Views/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Forgot Password Confirmation"; +} + +

@ViewData["Title"].

+

+ Please check your email to reset your password. +

diff --git a/IdentityApp/Views/Account/Lockout.cshtml b/IdentityApp/Views/Account/Lockout.cshtml new file mode 100644 index 0000000..2cc946d --- /dev/null +++ b/IdentityApp/Views/Account/Lockout.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Locked out"; +} + +
+

Locked out.

+

This account has been locked out, please try again later.

+
diff --git a/IdentityApp/Views/Account/LoggedOut.cshtml b/IdentityApp/Views/Account/LoggedOut.cshtml index 99599c0..481b9ad 100644 --- a/IdentityApp/Views/Account/LoggedOut.cshtml +++ b/IdentityApp/Views/Account/LoggedOut.cshtml @@ -1,8 +1,8 @@ @model LoggedOutViewModel @{ - // set this so the layout rendering sees an anonymous user - ViewData["signed-out"] = true; + // set this so UI rendering sees an anonymous user + Context.User = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity()); }