diff --git a/IdentityCore.sln b/IdentityCore.sln index 3971ae00b5..1741223af9 100644 --- a/IdentityCore.sln +++ b/IdentityCore.sln @@ -234,7 +234,6 @@ Global {D5FB2E24-4C71-430C-A289-59C8D59164B0}.Debug|x64.ActiveCfg = Debug|Any CPU {D5FB2E24-4C71-430C-A289-59C8D59164B0}.Debug|x64.Build.0 = Debug|Any CPU {D5FB2E24-4C71-430C-A289-59C8D59164B0}.Debug|x86.ActiveCfg = Debug|Any CPU - {D5FB2E24-4C71-430C-A289-59C8D59164B0}.Debug|x86.Build.0 = Debug|Any CPU {D5FB2E24-4C71-430C-A289-59C8D59164B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5FB2E24-4C71-430C-A289-59C8D59164B0}.Release|Any CPU.Build.0 = Release|Any CPU {D5FB2E24-4C71-430C-A289-59C8D59164B0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU @@ -282,7 +281,6 @@ Global {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Debug|x64.ActiveCfg = Debug|Any CPU {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Debug|x64.Build.0 = Debug|Any CPU {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Debug|x86.ActiveCfg = Debug|Any CPU - {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Debug|x86.Build.0 = Debug|Any CPU {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Release|Any CPU.Build.0 = Release|Any CPU {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU diff --git a/src/UI/Areas/Identity/Pages/Account/Login.cshtml b/src/UI/Areas/Identity/Pages/Account/Login.cshtml index da09191ae0..fe682d8d33 100644 --- a/src/UI/Areas/Identity/Pages/Account/Login.cshtml +++ b/src/UI/Areas/Identity/Pages/Account/Login.cshtml @@ -9,7 +9,7 @@
-
+

Use a local account to log in.


@@ -61,7 +61,7 @@ } else { - +

@foreach (var provider in Model.ExternalLogins) diff --git a/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs b/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs index 61ac948ae2..08bb305fb7 100644 --- a/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs +++ b/test/Identity.FunctionalTests/Extensions/HttpClientExtensions.cs @@ -1,10 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using AngleSharp.Dom.Html; using Xunit; @@ -16,7 +14,27 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests public static Task SendAsync( this HttpClient client, IHtmlFormElement form, - IEnumerable> formValues) + IHtmlElement submitButton) + { + return client.SendAsync(form, submitButton, new Dictionary()); + } + + public static Task SendAsync( + this HttpClient client, + IHtmlFormElement form, + IEnumerable> formValues) + { + var submitElement = Assert.Single(form.QuerySelectorAll("[type=submit]")); + var submitButton = Assert.IsAssignableFrom(submitElement); + + return client.SendAsync(form, submitButton, formValues); + } + + public static Task SendAsync( + this HttpClient client, + IHtmlFormElement form, + IHtmlElement submitButton, + IEnumerable> formValues) { foreach (var kvp in formValues) { @@ -24,9 +42,6 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests element.Value = kvp.Value; } - var submitElement = Assert.Single(form.QuerySelectorAll("[type=submit]")); - var submitButton = Assert.IsAssignableFrom(submitElement); - var submit = form.GetSubmission(submitButton); var submision = new HttpRequestMessage(new HttpMethod(submit.Method.ToString()), submit.Target) { diff --git a/test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs b/test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs new file mode 100644 index 0000000000..de4484ba75 --- /dev/null +++ b/test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests +{ + public class DefaultUIContext : HtmlPageContext + { + public DefaultUIContext() + { + } + + public DefaultUIContext(DefaultUIContext currentContext) + : base(currentContext) + { + + } + + public DefaultUIContext WithAuthenticatedUser() => + new DefaultUIContext(this) { UserAuthenticated = true }; + + public DefaultUIContext WithSocialLoginEnabled() => + new DefaultUIContext(this) { ContosoLoginEnabled = true }; + + public DefaultUIContext WithExistingUser() => + new DefaultUIContext(this) { ExistingUser = true }; + + public string AuthenticatorKey + { + get => GetValue(nameof(AuthenticatorKey)); + set => SetValue(nameof(AuthenticatorKey), value); + } + + public string[] RecoveryCodes + { + get => GetValue(nameof(RecoveryCodes)); + set => SetValue(nameof(RecoveryCodes), value); + } + + public bool TwoFactorEnabled + { + get => GetValue(nameof(TwoFactorEnabled)); + set => SetValue(nameof(TwoFactorEnabled), value); + } + public bool ContosoLoginEnabled + { + get => GetValue(nameof(ContosoLoginEnabled)); + set => SetValue(nameof(ContosoLoginEnabled), value); + } + + public bool UserAuthenticated + { + get => GetValue(nameof(UserAuthenticated)); + set => SetValue(nameof(UserAuthenticated), value); + } + public bool ExistingUser + { + get => GetValue(nameof(ExistingUser)); + set => SetValue(nameof(ExistingUser), value); + } + } +} diff --git a/test/Identity.FunctionalTests/Infrastructure/DefaultUIPage.cs b/test/Identity.FunctionalTests/Infrastructure/DefaultUIPage.cs new file mode 100644 index 0000000000..6c6c4d3288 --- /dev/null +++ b/test/Identity.FunctionalTests/Infrastructure/DefaultUIPage.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests +{ + public class DefaultUIPage : HtmlPage + { + public DefaultUIPage(HttpClient client, IHtmlDocument document, DefaultUIContext context) + : base(client, document, context) + { + } + } +} diff --git a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs index 15b5cbe4c3..ec2c97ce81 100644 --- a/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs +++ b/test/Identity.FunctionalTests/Infrastructure/FunctionalTestsServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Identity.DefaultUI.WebSite; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -9,12 +10,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests { public static class FunctionalTestsServiceCollectionExtensions { - public static IServiceCollection SetupTestDatabase(this IServiceCollection services, string databaseName) - { + public static IServiceCollection SetupTestDatabase(this IServiceCollection services, string databaseName) => services.AddDbContext(options => options.UseInMemoryDatabase(databaseName, memoryOptions => { })); - return services; - } + public static IServiceCollection SetupTestThirdPartyLogin(this IServiceCollection services) => + services.AddAuthentication() + .AddContosoAuthentication(o => o.SignInScheme = IdentityConstants.ExternalScheme) + .Services; } } diff --git a/test/Identity.FunctionalTests/Infrastructure/HtmlPage.cs b/test/Identity.FunctionalTests/Infrastructure/HtmlPage.cs index 8a2b1c5f45..7c5ac23757 100644 --- a/test/Identity.FunctionalTests/Infrastructure/HtmlPage.cs +++ b/test/Identity.FunctionalTests/Infrastructure/HtmlPage.cs @@ -6,9 +6,9 @@ using AngleSharp.Dom.Html; namespace Microsoft.AspNetCore.Identity.FunctionalTests { - public class HtmlPage + public class HtmlPage { - public HtmlPage(HttpClient client, IHtmlDocument document, HtmlPageContext context) + public HtmlPage(HttpClient client, IHtmlDocument document, TApplicationContext context) { Client = client; Document = document; @@ -17,6 +17,6 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests public HttpClient Client { get; } public IHtmlDocument Document { get; } - public HtmlPageContext Context { get; } + public TApplicationContext Context { get; } } } diff --git a/test/Identity.FunctionalTests/Infrastructure/HtmlPageContext.cs b/test/Identity.FunctionalTests/Infrastructure/HtmlPageContext.cs index eb15cd8873..7884616c76 100644 --- a/test/Identity.FunctionalTests/Infrastructure/HtmlPageContext.cs +++ b/test/Identity.FunctionalTests/Infrastructure/HtmlPageContext.cs @@ -7,13 +7,26 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests { public class HtmlPageContext { - private readonly IDictionary _properties = - new Dictionary(); + private readonly IDictionary _properties; - public string this[string key] + protected HtmlPageContext() + : this(new Dictionary()) { - get => _properties[key]; - set => _properties[key] = value; } + + protected HtmlPageContext(HtmlPageContext currentContext) + : this(new Dictionary(currentContext._properties)) + { + } + + private HtmlPageContext(IDictionary properties) + { + _properties = properties; + } + + protected TValue GetValue(string key) => + _properties.TryGetValue(key, out var rawValue) ? (TValue)rawValue : default; + protected void SetValue(string key, object value) => + _properties[key] = value; } } \ No newline at end of file diff --git a/test/Identity.FunctionalTests/LoginTests.cs b/test/Identity.FunctionalTests/LoginTests.cs index e923b57781..1b2a326657 100644 --- a/test/Identity.FunctionalTests/LoginTests.cs +++ b/test/Identity.FunctionalTests/LoginTests.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using System.Threading.Tasks; using AngleSharp.Dom.Html; -using Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -45,9 +44,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests var password = $"!Test.Password1$"; var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password); - var showRecoveryCodes = await UserStories.EnableTwoFactorAuthentication(loggedIn, twoFactorEnabled: false); + var showRecoveryCodes = await UserStories.EnableTwoFactorAuthentication(loggedIn); - var twoFactorKey = showRecoveryCodes.Context[EnableAuthenticator.AuthenticatorKey]; + var twoFactorKey = showRecoveryCodes.Context.AuthenticatorKey; // Act & Assert // Use a new client to simulate a new browser session. @@ -66,11 +65,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests var password = $"!Test.Password1$"; var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password); - var showRecoveryCodes = await UserStories.EnableTwoFactorAuthentication(loggedIn, twoFactorEnabled: false); + var showRecoveryCodes = await UserStories.EnableTwoFactorAuthentication(loggedIn); - var recoveryCode = showRecoveryCodes.Context[ShowRecoveryCodes.RecoveryCodes] - .Split(' ') - .First(); + var recoveryCode = showRecoveryCodes.Context.RecoveryCodes.First(); // Act & Assert // Use a new client to simulate a new browser session. @@ -131,6 +128,24 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests await UserStories.LoginExistingUserAsync(newClient, userName, password); } + + [Fact] + public async Task CanLoginWithASocialLoginProvider() + { + // Arrange + var server = ServerFactory.CreateServer(builder => + builder.ConfigureServices(services => services.SetupTestThirdPartyLogin())); + var client = ServerFactory.CreateDefaultClient(server); + var newClient = ServerFactory.CreateDefaultClient(server); + + var guid = Guid.NewGuid(); + var userName = $"{guid}"; + var email = $"{guid}@example.com"; + + // Act & Assert + await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email); + await UserStories.LoginWithSocialLoginAsync(newClient, userName); + } } class TestEmailSender : IEmailSender diff --git a/test/Identity.FunctionalTests/ManagementTests.cs b/test/Identity.FunctionalTests/ManagementTests.cs index ac57738ca7..f871994787 100644 --- a/test/Identity.FunctionalTests/ManagementTests.cs +++ b/test/Identity.FunctionalTests/ManagementTests.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests var index = await UserStories.RegisterNewUserAsync(client, userName, password); // Act & Assert - await UserStories.EnableTwoFactorAuthentication(index, twoFactorEnabled: false); + await UserStories.EnableTwoFactorAuthentication(index); } } } diff --git a/test/Identity.FunctionalTests/Pages/Account/ExternalLogin.cs b/test/Identity.FunctionalTests/Pages/Account/ExternalLogin.cs new file mode 100644 index 0000000000..7a10597aa0 --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Account/ExternalLogin.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Pages.Account +{ + public class ExternalLogin : DefaultUIPage + { + private readonly IHtmlFormElement _emailForm; + + public ExternalLogin( + HttpClient client, + IHtmlDocument externalLogin, + DefaultUIContext context) + : base(client, externalLogin, context) + { + _emailForm = HtmlAssert.HasForm(Document); + } + + public async Task SendEmailAsync(string email) + { + var response = await Client.SendAsync(_emailForm, new Dictionary + { + ["Input_Email"] = email + }); + var goToIndex = ResponseAssert.IsRedirect(response); + var indexResponse = await Client.GetAsync(goToIndex); + var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse); + + return new Index(Client, index, Context.WithAuthenticatedUser()); + } + } +} diff --git a/test/Identity.FunctionalTests/Pages/Account/Login.cs b/test/Identity.FunctionalTests/Pages/Account/Login.cs index 1275ac599c..a5792a279b 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Login.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Login.cs @@ -9,14 +9,35 @@ using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account { - public class Login : HtmlPage + public class Login : DefaultUIPage { private readonly IHtmlFormElement _loginForm; + private readonly IHtmlFormElement _externalLoginForm; + private readonly IHtmlElement _contosoButton; - public Login(HttpClient client, IHtmlDocument login, HtmlPageContext context) + public Login( + HttpClient client, + IHtmlDocument login, + DefaultUIContext context) : base(client, login, context) { - _loginForm = HtmlAssert.HasForm(login); + _loginForm = HtmlAssert.HasForm("#account", login); + if (Context.ContosoLoginEnabled) + { + _externalLoginForm = HtmlAssert.HasForm("#external-account", login); + _contosoButton = HtmlAssert.HasElement("button[value=Contoso]", login); + } + } + + public async Task ClickLoginWithContosoLinkAsync() + { + var externalFormResponse = await Client.SendAsync(_externalLoginForm, _contosoButton); + var goToContosoLogin = ResponseAssert.IsRedirect(externalFormResponse); + var contosoLoginResponse = await Client.GetAsync(goToContosoLogin); + + var contosoLogin = await ResponseAssert.IsHtmlDocumentAsync(contosoLoginResponse); + + return new Contoso.Login(Client, contosoLogin, Context); } public async Task LoginValidUserAsync(string userName, string password) @@ -27,7 +48,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account Assert.Equal(Index.Path, loggedInLocation.ToString()); var indexResponse = await Client.GetAsync(loggedInLocation); var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse); - return new Index(Client, index, Context, authenticated: true); + return new Index( + Client, + index, + Context.WithAuthenticatedUser()); } private async Task SendLoginForm(string userName, string password) diff --git a/test/Identity.FunctionalTests/Pages/Account/LoginWith2fa.cs b/test/Identity.FunctionalTests/Pages/Account/LoginWith2fa.cs index 8016352893..921d0da6d2 100644 --- a/test/Identity.FunctionalTests/Pages/Account/LoginWith2fa.cs +++ b/test/Identity.FunctionalTests/Pages/Account/LoginWith2fa.cs @@ -10,14 +10,14 @@ using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account { - public class LoginWith2fa : HtmlPage + public class LoginWith2fa : DefaultUIPage { public const string Path = "/Identity/Account/LoginWith2fa"; private readonly IHtmlFormElement _twoFactorForm; private readonly IHtmlAnchorElement _loginWithRecoveryCodeLink; - public LoginWith2fa(HttpClient client, IHtmlDocument loginWithTwoFactor, HtmlPageContext context) + public LoginWith2fa(HttpClient client, IHtmlDocument loginWithTwoFactor, DefaultUIContext context) : base(client, loginWithTwoFactor, context) { _twoFactorForm = HtmlAssert.HasForm(loginWithTwoFactor); @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account var indexResponse = await Client.GetAsync(goToIndex); var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse); - return new Index(Client, index, Context, true); + return new Index(Client, index, Context.WithAuthenticatedUser()); } internal async Task ClickRecoveryCodeLinkAsync() diff --git a/test/Identity.FunctionalTests/Pages/Account/LoginWithRecoveryCode.cs b/test/Identity.FunctionalTests/Pages/Account/LoginWithRecoveryCode.cs index 95b2c58b01..f5ee768809 100644 --- a/test/Identity.FunctionalTests/Pages/Account/LoginWithRecoveryCode.cs +++ b/test/Identity.FunctionalTests/Pages/Account/LoginWithRecoveryCode.cs @@ -8,11 +8,11 @@ using AngleSharp.Dom.Html; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account { - public class LoginWithRecoveryCode : HtmlPage + public class LoginWithRecoveryCode : DefaultUIPage { private readonly IHtmlFormElement _loginWithRecoveryCodeForm; - public LoginWithRecoveryCode(HttpClient client, IHtmlDocument loginWithRecoveryCode, HtmlPageContext context) + public LoginWithRecoveryCode(HttpClient client, IHtmlDocument loginWithRecoveryCode, DefaultUIContext context) : base(client, loginWithRecoveryCode, context) { _loginWithRecoveryCodeForm = HtmlAssert.HasForm(loginWithRecoveryCode); @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account var indexPage = await Client.GetAsync(goToIndex); var index = await ResponseAssert.IsHtmlDocumentAsync(indexPage); - return new Index(Client, index, Context, authenticated: true); + return new Index(Client, index, new DefaultUIContext(Context) { UserAuthenticated = true }); } } } \ No newline at end of file diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs index bae3e319f3..53003d7c8e 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/EnableAuthenticator.cs @@ -7,19 +7,24 @@ using System.Net.Http; using System.Security.Cryptography; using System.Threading.Tasks; using AngleSharp.Dom.Html; +using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage { - internal class EnableAuthenticator : HtmlPage + internal class EnableAuthenticator : DefaultUIPage { public const string AuthenticatorKey = nameof(EnableAuthenticator) + "." + nameof(AuthenticatorKey); private readonly IHtmlElement _codeElement; private readonly IHtmlFormElement _sendCodeForm; - public EnableAuthenticator(HttpClient client, IHtmlDocument enableAuthenticator, HtmlPageContext context) + public EnableAuthenticator( + HttpClient client, + IHtmlDocument enableAuthenticator, + DefaultUIContext context) : base(client, enableAuthenticator, context) { + Assert.True(Context.UserAuthenticated); _codeElement = HtmlAssert.HasElement("kbd", enableAuthenticator); _sendCodeForm = HtmlAssert.HasForm("#send-code", enableAuthenticator); } @@ -27,7 +32,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage internal async Task SendValidCodeAsync() { var authenticatorKey = _codeElement.TextContent.Replace(" ", ""); - Context[AuthenticatorKey] = authenticatorKey; + Context.AuthenticatorKey = authenticatorKey; var verificationCode = ComputeCode(authenticatorKey); var sendCodeResponse = await Client.SendAsync(_sendCodeForm, new Dictionary diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs index 270563de47..20e9b81356 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs @@ -4,31 +4,34 @@ using System.Net.Http; using System.Threading.Tasks; using AngleSharp.Dom.Html; +using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage { - public class Index : HtmlPage + public class Index : DefaultUIPage { private readonly IHtmlAnchorElement _profileLink; private readonly IHtmlAnchorElement _changePasswordLink; private readonly IHtmlAnchorElement _twoFactorLink; private readonly IHtmlAnchorElement _personalDataLink; - public Index(HttpClient client, IHtmlDocument manage, HtmlPageContext context) + public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context) : base(client, manage, context) { + Assert.True(Context.UserAuthenticated); + _profileLink = HtmlAssert.HasLink("#profile", manage); _changePasswordLink = HtmlAssert.HasLink("#change-password", manage); _twoFactorLink = HtmlAssert.HasLink("#two-factor", manage); _personalDataLink = HtmlAssert.HasLink("#personal-data", manage); } - public async Task ClickTwoFactorLinkAsync(bool twoFactorEnabled) + public async Task ClickTwoFactorLinkAsync() { var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href); var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor); - return new TwoFactorAuthentication(Client, twoFactor, Context, twoFactorEnabled); + return new TwoFactorAuthentication(Client, twoFactor, Context); } } } diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/ShowRecoveryCodes.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/ShowRecoveryCodes.cs index f4601d6fb6..1fbc98fa04 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Manage/ShowRecoveryCodes.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/ShowRecoveryCodes.cs @@ -8,17 +8,15 @@ using AngleSharp.Dom.Html; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage { - internal class ShowRecoveryCodes : HtmlPage + internal class ShowRecoveryCodes : DefaultUIPage { - public const string RecoveryCodes = nameof(ShowRecoveryCodes) + "." + nameof(RecoveryCodes); - private readonly IEnumerable _recoveryCodeElements; - public ShowRecoveryCodes(HttpClient client, IHtmlDocument showRecoveryCodes, HtmlPageContext context) + public ShowRecoveryCodes(HttpClient client, IHtmlDocument showRecoveryCodes, DefaultUIContext context) : base(client, showRecoveryCodes, context) { _recoveryCodeElements = HtmlAssert.HasElements(".recovery-code", showRecoveryCodes); - Context[RecoveryCodes] = string.Join(" ", Codes); + Context.RecoveryCodes = Codes.ToArray(); } public IEnumerable Codes => _recoveryCodeElements.Select(rc => rc.TextContent); diff --git a/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs b/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs index f280c28fc6..1faec56c1b 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs @@ -8,16 +8,14 @@ using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage { - public class TwoFactorAuthentication : HtmlPage + public class TwoFactorAuthentication : DefaultUIPage { - private readonly bool _twoFactorEnabled; private readonly IHtmlAnchorElement _enableAuthenticatorLink; - public TwoFactorAuthentication(HttpClient client, IHtmlDocument twoFactor, HtmlPageContext context, bool twoFactorEnabled) + public TwoFactorAuthentication(HttpClient client, IHtmlDocument twoFactor, DefaultUIContext context) : base(client, twoFactor, context) { - _twoFactorEnabled = twoFactorEnabled; - if (!_twoFactorEnabled) + if (!Context.TwoFactorEnabled) { _enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor); } @@ -25,7 +23,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage internal async Task ClickEnableAuthenticatorLinkAsync() { - Assert.False(_twoFactorEnabled); + Assert.False(Context.TwoFactorEnabled); var goToEnableAuthenticator = await Client.GetAsync(_enableAuthenticatorLink.Href); var enableAuthenticator = await ResponseAssert.IsHtmlDocumentAsync(goToEnableAuthenticator); diff --git a/test/Identity.FunctionalTests/Pages/Account/Register.cs b/test/Identity.FunctionalTests/Pages/Account/Register.cs index 0c1ab8e931..41afb55fc6 100644 --- a/test/Identity.FunctionalTests/Pages/Account/Register.cs +++ b/test/Identity.FunctionalTests/Pages/Account/Register.cs @@ -9,11 +9,11 @@ using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account { - public class Register : HtmlPage + public class Register : DefaultUIPage { private IHtmlFormElement _registerForm; - public Register(HttpClient client, IHtmlDocument register, HtmlPageContext context) + public Register(HttpClient client, IHtmlDocument register, DefaultUIContext context) : base(client, register, context) { _registerForm = HtmlAssert.HasForm(register); @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account var indexResponse = await Client.GetAsync(registeredLocation); var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse); - return new Index(Client, index, Context, authenticated: true); + return new Index(Client, index, Context.WithAuthenticatedUser()); } } } diff --git a/test/Identity.FunctionalTests/Pages/Contoso/Login.cs b/test/Identity.FunctionalTests/Pages/Contoso/Login.cs new file mode 100644 index 0000000000..c9e22a85f4 --- /dev/null +++ b/test/Identity.FunctionalTests/Pages/Contoso/Login.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using AngleSharp.Dom.Html; +using Microsoft.AspNetCore.Identity.FunctionalTests.Pages.Account; + +namespace Microsoft.AspNetCore.Identity.FunctionalTests.Contoso +{ + public class Login : DefaultUIPage + { + private readonly IHtmlFormElement _loginForm; + + public Login(HttpClient client, IHtmlDocument login, DefaultUIContext context) + : base(client, login, context) + { + _loginForm = HtmlAssert.HasForm(login); + } + + public async Task SendNewUserNameAsync(string userName) + { + var externalLogin = await SendLoginForm(userName); + + return new ExternalLogin(Client, externalLogin, Context); + } + + public async Task SendExistingUserNameAsync(string userName) + { + var externalLogin = await SendLoginForm(userName); + + return new Index(Client, externalLogin, Context.WithAuthenticatedUser()); + } + + private async Task SendLoginForm(string userName) + { + var contosoResponse = await Client.SendAsync(_loginForm, new Dictionary + { + ["Input_Login"] = userName + }); + + var goToExternalLogin = ResponseAssert.IsRedirect(contosoResponse); + var externalLogInResponse = await Client.GetAsync(goToExternalLogin); + if (Context.ExistingUser) + { + var goToIndex = ResponseAssert.IsRedirect(externalLogInResponse); + var indexResponse = await Client.GetAsync(goToIndex); + return await ResponseAssert.IsHtmlDocumentAsync(indexResponse); + } + else + { + return await ResponseAssert.IsHtmlDocumentAsync(externalLogInResponse); + } + } + } +} diff --git a/test/Identity.FunctionalTests/Pages/Index.cs b/test/Identity.FunctionalTests/Pages/Index.cs index 447754daac..71fdd9e7fa 100644 --- a/test/Identity.FunctionalTests/Pages/Index.cs +++ b/test/Identity.FunctionalTests/Pages/Index.cs @@ -9,19 +9,20 @@ using Xunit; namespace Microsoft.AspNetCore.Identity.FunctionalTests { - public class Index : HtmlPage + public class Index : DefaultUIPage { - private readonly bool _authenticated; private readonly IHtmlAnchorElement _registerLink; private readonly IHtmlAnchorElement _loginLink; private readonly IHtmlAnchorElement _manageLink; public static readonly string Path = "/"; - public Index(HttpClient client, IHtmlDocument index, HtmlPageContext context, bool authenticated) + public Index( + HttpClient client, + IHtmlDocument index, + DefaultUIContext context) : base(client, index, context) { - _authenticated = authenticated; - if (!_authenticated) + if (!Context.UserAuthenticated) { _registerLink = HtmlAssert.HasLink("#register", Document); _loginLink = HtmlAssert.HasLink("#login", Document); @@ -32,17 +33,17 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests } } - public static async Task CreateAsync(HttpClient client, bool authenticated = false) + public static async Task CreateAsync(HttpClient client, DefaultUIContext context = null) { var goToIndex = await client.GetAsync("/"); var index = await ResponseAssert.IsHtmlDocumentAsync(goToIndex); - return new Index(client, index, new HtmlPageContext(), authenticated); + return new Index(client, index, context ?? new DefaultUIContext()); } public async Task ClickRegisterLinkAsync() { - Assert.False(_authenticated); + Assert.False(Context.UserAuthenticated); var goToRegister = await Client.GetAsync(_registerLink.Href); var register = await ResponseAssert.IsHtmlDocumentAsync(goToRegister); @@ -52,7 +53,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests public async Task ClickLoginLinkAsync() { - Assert.False(_authenticated); + Assert.False(Context.UserAuthenticated); var goToLogin = await Client.GetAsync(_loginLink.Href); var login = await ResponseAssert.IsHtmlDocumentAsync(goToLogin); @@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests internal async Task ClickManageLinkAsync() { - Assert.True(_authenticated); + Assert.True(Context.UserAuthenticated); var goToManage = await Client.GetAsync(_manageLink.Href); var manage = await ResponseAssert.IsHtmlDocumentAsync(goToManage); diff --git a/test/Identity.FunctionalTests/RegistrationTests.cs b/test/Identity.FunctionalTests/RegistrationTests.cs index d6a4c7c450..f41f40ac98 100644 --- a/test/Identity.FunctionalTests/RegistrationTests.cs +++ b/test/Identity.FunctionalTests/RegistrationTests.cs @@ -21,5 +21,21 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests // Act & Assert await UserStories.RegisterNewUserAsync(client, userName, password); } + + [Fact] + public async Task CanRegisterWithASocialLoginProvider() + { + // Arrange + var server = ServerFactory.CreateServer(builder => + builder.ConfigureServices(services => services.SetupTestThirdPartyLogin())); + var client = ServerFactory.CreateDefaultClient(server); + + var guid = Guid.NewGuid(); + var userName = $"{guid}"; + var email = $"{guid}@example.com"; + + // Act & Assert + await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email); + } } } diff --git a/test/Identity.FunctionalTests/UserStories.cs b/test/Identity.FunctionalTests/UserStories.cs index 63679b9b7c..cb81ac3a2b 100644 --- a/test/Identity.FunctionalTests/UserStories.cs +++ b/test/Identity.FunctionalTests/UserStories.cs @@ -30,6 +30,34 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return await login.LoginValidUserAsync(userName, password); } + internal static async Task RegisterNewUserWithSocialLoginAsync(HttpClient client, string userName, string email) + { + var index = await Index.CreateAsync(client,new DefaultUIContext().WithSocialLoginEnabled()); + + var login = await index.ClickLoginLinkAsync(); + + var contosoLogin = await login.ClickLoginWithContosoLinkAsync(); + + var externalLogin = await contosoLogin.SendNewUserNameAsync(userName); + + return await externalLogin.SendEmailAsync(email); + } + + internal static async Task LoginWithSocialLoginAsync(HttpClient client, string userName) + { + var index = await Index.CreateAsync( + client, + new DefaultUIContext() + .WithSocialLoginEnabled() + .WithExistingUser()); + + var login = await index.ClickLoginLinkAsync(); + + var contosoLogin = await login.ClickLoginWithContosoLinkAsync(); + + return await contosoLogin.SendExistingUserNameAsync(userName); + } + internal static async Task LoginExistingUser2FaAsync(HttpClient client, string userName, string password, string twoFactorKey) { var index = await Index.CreateAsync(client); @@ -41,12 +69,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests return await login2Fa.Send2FACodeAsync(twoFactorKey); } - internal static async Task EnableTwoFactorAuthentication( - Index index, - bool twoFactorEnabled) + internal static async Task EnableTwoFactorAuthentication(Index index) { var manage = await index.ClickManageLinkAsync(); - var twoFactor = await manage.ClickTwoFactorLinkAsync(twoFactorEnabled); + var twoFactor = await manage.ClickTwoFactorLinkAsync(); var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync(); return await enableAuthenticator.SendValidCodeAsync(); } @@ -63,7 +89,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests var login2Fa = await loginWithPassword.PasswordLoginValidUserWith2FaAsync(userName, password); - var loginRecoveryCode = await login2Fa.ClickRecoveryCodeLinkAsync(); + var loginRecoveryCode = await login2Fa.ClickRecoveryCodeLinkAsync(); return await loginRecoveryCode.SendRecoveryCodeAsync(recoveryCode); } diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/20180217170630_UpdateIdentitySchema.Designer.cs b/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/20180217170630_UpdateIdentitySchema.Designer.cs new file mode 100644 index 0000000000..3a5dc113b2 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/20180217170630_UpdateIdentitySchema.Designer.cs @@ -0,0 +1,231 @@ +// +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; + +namespace Identity.DefaultUI.WebSite.Data.Migrations +{ + [DbContext(typeof(IdentityDbContext))] + [Migration("20180217170630_UpdateIdentitySchema")] + partial class UpdateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.0-preview2-30103") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.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.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.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.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.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/20180217170630_UpdateIdentitySchema.cs b/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/20180217170630_UpdateIdentitySchema.cs new file mode 100644 index 0000000000..e99ded8a85 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/20180217170630_UpdateIdentitySchema.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Identity.DefaultUI.WebSite.Data.Migrations +{ + public partial class UpdateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "UserNameIndex", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "IX_AspNetUserRoles_UserId", + table: "AspNetUserRoles"); + + migrationBuilder.DropIndex( + name: "RoleNameIndex", + table: "AspNetRoles"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.AddForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + table: "AspNetUserTokens", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + table: "AspNetUserTokens"); + + migrationBuilder.DropIndex( + name: "UserNameIndex", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "RoleNameIndex", + table: "AspNetRoles"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_UserId", + table: "AspNetUserRoles", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName"); + } + } +} diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/IdentityDbContextModelSnapshot.cs b/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/IdentityDbContextModelSnapshot.cs index feafe9a295..78593d82d0 100644 --- a/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/IdentityDbContextModelSnapshot.cs +++ b/test/WebSites/Identity.DefaultUI.WebSite/Data/Migrations/IdentityDbContextModelSnapshot.cs @@ -1,11 +1,13 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - +// using System; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; namespace Identity.DefaultUI.WebSite.Data.Migrations { @@ -14,32 +16,36 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations { protected override void BuildModel(ModelBuilder modelBuilder) { +#pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("ProductVersion", "2.1.0-preview2-30103") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { - b.Property("Id"); + b.Property("Id") + .ValueGeneratedOnAdd(); b.Property("ConcurrencyStamp") .IsConcurrencyToken(); b.Property("Name") - .HasAnnotation("MaxLength", 256); + .HasMaxLength(256); b.Property("NormalizedName") - .HasAnnotation("MaxLength", 256); + .HasMaxLength(256); b.HasKey("Id"); b.HasIndex("NormalizedName") - .HasName("RoleNameIndex"); + .IsUnique() + .HasName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); b.ToTable("AspNetRoles"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.Property("Id") .ValueGeneratedOnAdd(); @@ -58,7 +64,58 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations b.ToTable("AspNetRoleClaims"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.Property("Id") .ValueGeneratedOnAdd(); @@ -77,7 +134,7 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations b.ToTable("AspNetUserClaims"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.Property("LoginProvider"); @@ -95,7 +152,7 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations b.ToTable("AspNetUserLogins"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { b.Property("UserId"); @@ -105,12 +162,10 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations b.HasIndex("RoleId"); - b.HasIndex("UserId"); - b.ToTable("AspNetUserRoles"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { b.Property("UserId"); @@ -125,91 +180,51 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations b.ToTable("AspNetUserTokens"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", 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") + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany("Claims") + .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany("Logins") + .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => { - b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") - .WithMany("Users") + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() .HasForeignKey("RoleId") .OnDelete(DeleteBehavior.Cascade); b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany("Roles") + .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade); }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 } } } diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Identity.DefaultUI.WebSite.csproj b/test/WebSites/Identity.DefaultUI.WebSite/Identity.DefaultUI.WebSite.csproj index db6eb71f42..9748c31546 100644 --- a/test/WebSites/Identity.DefaultUI.WebSite/Identity.DefaultUI.WebSite.csproj +++ b/test/WebSites/Identity.DefaultUI.WebSite/Identity.DefaultUI.WebSite.csproj @@ -39,4 +39,8 @@ + + + + \ No newline at end of file diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml b/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml new file mode 100644 index 0000000000..cb6572a8c6 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml @@ -0,0 +1,37 @@ +@page +@model LoginModel +@{ + Layout = "_Layout"; + ViewData["Title"] = "Contoso log-in"; +} + +

@ViewData["Title"]

+
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml.cs b/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml.cs new file mode 100644 index 0000000000..a99f1f7ff4 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Pages/Contoso/Login.cshtml.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Identity.DefaultUI.WebSite.Pages +{ + public class LoginModel : PageModel + { + public LoginModel(IOptionsMonitor options) + { + Options = options.CurrentValue; + } + + public class InputModel + { + [Required] + public string Login { get; set; } + public bool RememberMe { get; set; } + } + + [BindProperty] + public InputModel Input { get; set; } + + [BindProperty(SupportsGet = true)] + public string ReturnUrl { get; set; } + + [BindProperty] + public string State { get; set; } + + public ContosoAuthenticationOptions Options { get; } + + public IActionResult OnGet() + { + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + else + { + var state = JsonConvert.DeserializeObject>(State); + var identity = new ClaimsIdentity(new Claim[] + { + new Claim(ClaimTypes.NameIdentifier, Input.Login) + }, + state["LoginProvider"], + ClaimTypes.NameIdentifier, + ClaimTypes.Role); + var principal = new ClaimsPrincipal(identity); + var properties = new AuthenticationProperties(state) + { + IsPersistent = Input.RememberMe + }; + await HttpContext.SignInAsync(Options.SignInScheme, principal, properties); + return Redirect(ReturnUrl ?? "~/"); + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationBuilderExtensions.cs b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000000..0e64aaf5a8 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationBuilderExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; + +namespace Identity.DefaultUI.WebSite +{ + public static class ContosoAuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddContosoAuthentication( + this AuthenticationBuilder builder, + Action configure) => + builder.AddScheme( + ContosoAuthenticationConstants.Scheme, + ContosoAuthenticationConstants.DisplayName, + configure); + } +} diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationConstants.cs b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationConstants.cs new file mode 100644 index 0000000000..229e80c592 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationConstants.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Identity.DefaultUI.WebSite +{ + public static class ContosoAuthenticationConstants + { + public const string Scheme = "Contoso"; + public const string DisplayName = "Contoso"; + } +} \ No newline at end of file diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationHandler.cs b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationHandler.cs new file mode 100644 index 0000000000..3ac1667244 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationHandler.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Identity.DefaultUI.WebSite +{ + public class ContosoAuthenticationHandler : AuthenticationHandler + { + public ContosoAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() => + Task.FromResult(AuthenticateResult.NoResult()); + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + var uri = $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Options.RemoteLoginPath}"; + uri = QueryHelpers.AddQueryString(uri, new Dictionary() + { + ["State"] = JsonConvert.SerializeObject(properties.Items), + [Options.ReturnUrlQueryParameter] = properties.RedirectUri + }); + Response.Redirect(uri); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationOptions.cs b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationOptions.cs new file mode 100644 index 0000000000..5c0de39d16 --- /dev/null +++ b/test/WebSites/Identity.DefaultUI.WebSite/Services/ContosoAuthenticationOptions.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication; + +namespace Identity.DefaultUI.WebSite +{ + public class ContosoAuthenticationOptions : AuthenticationSchemeOptions + { + public ContosoAuthenticationOptions() + { + Events = new object(); + } + + public string SignInScheme { get; set; } + public string ReturnUrlQueryParameter { get; set; } = "returnUrl"; + public string RemoteLoginPath { get; set; } = "/Contoso/Login"; + } +} \ No newline at end of file diff --git a/test/WebSites/Identity.DefaultUI.WebSite/appsettings.json b/test/WebSites/Identity.DefaultUI.WebSite/appsettings.json index 00e6b3cfce..acbebb34a4 100644 --- a/test/WebSites/Identity.DefaultUI.WebSite/appsettings.json +++ b/test/WebSites/Identity.DefaultUI.WebSite/appsettings.json @@ -1,6 +1,7 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Identity.DefaultUI.WebSite-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Identity.DefaultUI.WebSite-90455f3b-6c48-4aa0-a8d6-294d8e0b3d4d;Trusted_Connection=True;MultipleActiveResultSets=true" + //"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Identity.DefaultUI.WebSite-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Logging": { "LogLevel": {