Add an end to end test for login in with a social provider.

This commit is contained in:
Javier Calvarro Nelson 2018-02-17 09:02:15 -08:00
parent 5c105e6506
commit d1aa7d527b
34 changed files with 965 additions and 153 deletions

View File

@ -234,7 +234,6 @@ Global
{D5FB2E24-4C71-430C-A289-59C8D59164B0}.Debug|x64.ActiveCfg = Debug|Any CPU {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|x64.Build.0 = Debug|Any CPU
{D5FB2E24-4C71-430C-A289-59C8D59164B0}.Debug|x86.ActiveCfg = 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.ActiveCfg = Release|Any CPU
{D5FB2E24-4C71-430C-A289-59C8D59164B0}.Release|Any CPU.Build.0 = 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 {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.ActiveCfg = Debug|Any CPU
{EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Debug|x64.Build.0 = 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.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.ActiveCfg = Release|Any CPU
{EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Release|Any CPU.Build.0 = 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 {EA424B4D-0BE1-49AC-A106-CC6CC808A104}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU

View File

@ -9,7 +9,7 @@
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<section> <section>
<form method="post"> <form id="account" method="post">
<h4>Use a local account to log in.</h4> <h4>Use a local account to log in.</h4>
<hr /> <hr />
<div asp-validation-summary="All" class="text-danger"></div> <div asp-validation-summary="All" class="text-danger"></div>
@ -61,7 +61,7 @@
} }
else else
{ {
<form asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div> <div>
<p> <p>
@foreach (var provider in Model.ExternalLogins) @foreach (var provider in Model.ExternalLogins)

View File

@ -1,10 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Dom.Html; using AngleSharp.Dom.Html;
using Xunit; using Xunit;
@ -16,7 +14,27 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
public static Task<HttpResponseMessage> SendAsync( public static Task<HttpResponseMessage> SendAsync(
this HttpClient client, this HttpClient client,
IHtmlFormElement form, IHtmlFormElement form,
IEnumerable<KeyValuePair<string,string>> formValues) IHtmlElement submitButton)
{
return client.SendAsync(form, submitButton, new Dictionary<string, string>());
}
public static Task<HttpResponseMessage> SendAsync(
this HttpClient client,
IHtmlFormElement form,
IEnumerable<KeyValuePair<string, string>> formValues)
{
var submitElement = Assert.Single(form.QuerySelectorAll("[type=submit]"));
var submitButton = Assert.IsAssignableFrom<IHtmlElement>(submitElement);
return client.SendAsync(form, submitButton, formValues);
}
public static Task<HttpResponseMessage> SendAsync(
this HttpClient client,
IHtmlFormElement form,
IHtmlElement submitButton,
IEnumerable<KeyValuePair<string, string>> formValues)
{ {
foreach (var kvp in formValues) foreach (var kvp in formValues)
{ {
@ -24,9 +42,6 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
element.Value = kvp.Value; element.Value = kvp.Value;
} }
var submitElement = Assert.Single(form.QuerySelectorAll("[type=submit]"));
var submitButton = Assert.IsAssignableFrom<IHtmlElement>(submitElement);
var submit = form.GetSubmission(submitButton); var submit = form.GetSubmission(submitButton);
var submision = new HttpRequestMessage(new HttpMethod(submit.Method.ToString()), submit.Target) var submision = new HttpRequestMessage(new HttpMethod(submit.Method.ToString()), submit.Target)
{ {

View File

@ -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<string>(nameof(AuthenticatorKey));
set => SetValue(nameof(AuthenticatorKey), value);
}
public string[] RecoveryCodes
{
get => GetValue<string[]>(nameof(RecoveryCodes));
set => SetValue(nameof(RecoveryCodes), value);
}
public bool TwoFactorEnabled
{
get => GetValue<bool>(nameof(TwoFactorEnabled));
set => SetValue(nameof(TwoFactorEnabled), value);
}
public bool ContosoLoginEnabled
{
get => GetValue<bool>(nameof(ContosoLoginEnabled));
set => SetValue(nameof(ContosoLoginEnabled), value);
}
public bool UserAuthenticated
{
get => GetValue<bool>(nameof(UserAuthenticated));
set => SetValue(nameof(UserAuthenticated), value);
}
public bool ExistingUser
{
get => GetValue<bool>(nameof(ExistingUser));
set => SetValue(nameof(ExistingUser), value);
}
}
}

View File

@ -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<DefaultUIContext>
{
public DefaultUIPage(HttpClient client, IHtmlDocument document, DefaultUIContext context)
: base(client, document, context)
{
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -9,12 +10,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
{ {
public static class FunctionalTestsServiceCollectionExtensions public static class FunctionalTestsServiceCollectionExtensions
{ {
public static IServiceCollection SetupTestDatabase(this IServiceCollection services, string databaseName) public static IServiceCollection SetupTestDatabase(this IServiceCollection services, string databaseName) =>
{
services.AddDbContext<IdentityDbContext>(options => services.AddDbContext<IdentityDbContext>(options =>
options.UseInMemoryDatabase(databaseName, memoryOptions => { })); options.UseInMemoryDatabase(databaseName, memoryOptions => { }));
return services; public static IServiceCollection SetupTestThirdPartyLogin(this IServiceCollection services) =>
} services.AddAuthentication()
.AddContosoAuthentication(o => o.SignInScheme = IdentityConstants.ExternalScheme)
.Services;
} }
} }

View File

@ -6,9 +6,9 @@ using AngleSharp.Dom.Html;
namespace Microsoft.AspNetCore.Identity.FunctionalTests namespace Microsoft.AspNetCore.Identity.FunctionalTests
{ {
public class HtmlPage public class HtmlPage<TApplicationContext>
{ {
public HtmlPage(HttpClient client, IHtmlDocument document, HtmlPageContext context) public HtmlPage(HttpClient client, IHtmlDocument document, TApplicationContext context)
{ {
Client = client; Client = client;
Document = document; Document = document;
@ -17,6 +17,6 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
public HttpClient Client { get; } public HttpClient Client { get; }
public IHtmlDocument Document { get; } public IHtmlDocument Document { get; }
public HtmlPageContext Context { get; } public TApplicationContext Context { get; }
} }
} }

View File

@ -7,13 +7,26 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
{ {
public class HtmlPageContext public class HtmlPageContext
{ {
private readonly IDictionary<string, string> _properties = private readonly IDictionary<string, object> _properties;
new Dictionary<string, string>();
public string this[string key] protected HtmlPageContext()
: this(new Dictionary<string, object>())
{ {
get => _properties[key];
set => _properties[key] = value;
} }
protected HtmlPageContext(HtmlPageContext currentContext)
: this(new Dictionary<string,object>(currentContext._properties))
{
}
private HtmlPageContext(IDictionary<string, object> properties)
{
_properties = properties;
}
protected TValue GetValue<TValue>(string key) =>
_properties.TryGetValue(key, out var rawValue) ? (TValue)rawValue : default;
protected void SetValue(string key, object value) =>
_properties[key] = value;
} }
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Dom.Html; using AngleSharp.Dom.Html;
using Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage;
using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Xunit; using Xunit;
@ -45,9 +44,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
var password = $"!Test.Password1$"; var password = $"!Test.Password1$";
var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password); 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 // Act & Assert
// Use a new client to simulate a new browser session. // Use a new client to simulate a new browser session.
@ -66,11 +65,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
var password = $"!Test.Password1$"; var password = $"!Test.Password1$";
var loggedIn = await UserStories.RegisterNewUserAsync(client, userName, password); 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] var recoveryCode = showRecoveryCodes.Context.RecoveryCodes.First();
.Split(' ')
.First();
// Act & Assert // Act & Assert
// Use a new client to simulate a new browser session. // 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); 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 class TestEmailSender : IEmailSender

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
var index = await UserStories.RegisterNewUserAsync(client, userName, password); var index = await UserStories.RegisterNewUserAsync(client, userName, password);
// Act & Assert // Act & Assert
await UserStories.EnableTwoFactorAuthentication(index, twoFactorEnabled: false); await UserStories.EnableTwoFactorAuthentication(index);
} }
} }
} }

View File

@ -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<Index> SendEmailAsync(string email)
{
var response = await Client.SendAsync(_emailForm, new Dictionary<string, string>
{
["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());
}
}
}

View File

@ -9,14 +9,35 @@ using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
{ {
public class Login : HtmlPage public class Login : DefaultUIPage
{ {
private readonly IHtmlFormElement _loginForm; 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) : 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<Contoso.Login> 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<Index> LoginValidUserAsync(string userName, string password) public async Task<Index> LoginValidUserAsync(string userName, string password)
@ -27,7 +48,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
Assert.Equal(Index.Path, loggedInLocation.ToString()); Assert.Equal(Index.Path, loggedInLocation.ToString());
var indexResponse = await Client.GetAsync(loggedInLocation); var indexResponse = await Client.GetAsync(loggedInLocation);
var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse); var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse);
return new Index(Client, index, Context, authenticated: true); return new Index(
Client,
index,
Context.WithAuthenticatedUser());
} }
private async Task<HttpResponseMessage> SendLoginForm(string userName, string password) private async Task<HttpResponseMessage> SendLoginForm(string userName, string password)

View File

@ -10,14 +10,14 @@ using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
{ {
public class LoginWith2fa : HtmlPage public class LoginWith2fa : DefaultUIPage
{ {
public const string Path = "/Identity/Account/LoginWith2fa"; public const string Path = "/Identity/Account/LoginWith2fa";
private readonly IHtmlFormElement _twoFactorForm; private readonly IHtmlFormElement _twoFactorForm;
private readonly IHtmlAnchorElement _loginWithRecoveryCodeLink; private readonly IHtmlAnchorElement _loginWithRecoveryCodeLink;
public LoginWith2fa(HttpClient client, IHtmlDocument loginWithTwoFactor, HtmlPageContext context) public LoginWith2fa(HttpClient client, IHtmlDocument loginWithTwoFactor, DefaultUIContext context)
: base(client, loginWithTwoFactor, context) : base(client, loginWithTwoFactor, context)
{ {
_twoFactorForm = HtmlAssert.HasForm(loginWithTwoFactor); _twoFactorForm = HtmlAssert.HasForm(loginWithTwoFactor);
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
var indexResponse = await Client.GetAsync(goToIndex); var indexResponse = await Client.GetAsync(goToIndex);
var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse); var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse);
return new Index(Client, index, Context, true); return new Index(Client, index, Context.WithAuthenticatedUser());
} }
internal async Task<LoginWithRecoveryCode> ClickRecoveryCodeLinkAsync() internal async Task<LoginWithRecoveryCode> ClickRecoveryCodeLinkAsync()

View File

@ -8,11 +8,11 @@ using AngleSharp.Dom.Html;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
{ {
public class LoginWithRecoveryCode : HtmlPage public class LoginWithRecoveryCode : DefaultUIPage
{ {
private readonly IHtmlFormElement _loginWithRecoveryCodeForm; private readonly IHtmlFormElement _loginWithRecoveryCodeForm;
public LoginWithRecoveryCode(HttpClient client, IHtmlDocument loginWithRecoveryCode, HtmlPageContext context) public LoginWithRecoveryCode(HttpClient client, IHtmlDocument loginWithRecoveryCode, DefaultUIContext context)
: base(client, loginWithRecoveryCode, context) : base(client, loginWithRecoveryCode, context)
{ {
_loginWithRecoveryCodeForm = HtmlAssert.HasForm(loginWithRecoveryCode); _loginWithRecoveryCodeForm = HtmlAssert.HasForm(loginWithRecoveryCode);
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
var indexPage = await Client.GetAsync(goToIndex); var indexPage = await Client.GetAsync(goToIndex);
var index = await ResponseAssert.IsHtmlDocumentAsync(indexPage); var index = await ResponseAssert.IsHtmlDocumentAsync(indexPage);
return new Index(Client, index, Context, authenticated: true); return new Index(Client, index, new DefaultUIContext(Context) { UserAuthenticated = true });
} }
} }
} }

View File

@ -7,19 +7,24 @@ using System.Net.Http;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Dom.Html; using AngleSharp.Dom.Html;
using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
{ {
internal class EnableAuthenticator : HtmlPage internal class EnableAuthenticator : DefaultUIPage
{ {
public const string AuthenticatorKey = nameof(EnableAuthenticator) + "." + nameof(AuthenticatorKey); public const string AuthenticatorKey = nameof(EnableAuthenticator) + "." + nameof(AuthenticatorKey);
private readonly IHtmlElement _codeElement; private readonly IHtmlElement _codeElement;
private readonly IHtmlFormElement _sendCodeForm; private readonly IHtmlFormElement _sendCodeForm;
public EnableAuthenticator(HttpClient client, IHtmlDocument enableAuthenticator, HtmlPageContext context) public EnableAuthenticator(
HttpClient client,
IHtmlDocument enableAuthenticator,
DefaultUIContext context)
: base(client, enableAuthenticator, context) : base(client, enableAuthenticator, context)
{ {
Assert.True(Context.UserAuthenticated);
_codeElement = HtmlAssert.HasElement("kbd", enableAuthenticator); _codeElement = HtmlAssert.HasElement("kbd", enableAuthenticator);
_sendCodeForm = HtmlAssert.HasForm("#send-code", enableAuthenticator); _sendCodeForm = HtmlAssert.HasForm("#send-code", enableAuthenticator);
} }
@ -27,7 +32,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
internal async Task<ShowRecoveryCodes> SendValidCodeAsync() internal async Task<ShowRecoveryCodes> SendValidCodeAsync()
{ {
var authenticatorKey = _codeElement.TextContent.Replace(" ", ""); var authenticatorKey = _codeElement.TextContent.Replace(" ", "");
Context[AuthenticatorKey] = authenticatorKey; Context.AuthenticatorKey = authenticatorKey;
var verificationCode = ComputeCode(authenticatorKey); var verificationCode = ComputeCode(authenticatorKey);
var sendCodeResponse = await Client.SendAsync(_sendCodeForm, new Dictionary<string, string> var sendCodeResponse = await Client.SendAsync(_sendCodeForm, new Dictionary<string, string>

View File

@ -4,31 +4,34 @@
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using AngleSharp.Dom.Html; using AngleSharp.Dom.Html;
using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
{ {
public class Index : HtmlPage public class Index : DefaultUIPage
{ {
private readonly IHtmlAnchorElement _profileLink; private readonly IHtmlAnchorElement _profileLink;
private readonly IHtmlAnchorElement _changePasswordLink; private readonly IHtmlAnchorElement _changePasswordLink;
private readonly IHtmlAnchorElement _twoFactorLink; private readonly IHtmlAnchorElement _twoFactorLink;
private readonly IHtmlAnchorElement _personalDataLink; private readonly IHtmlAnchorElement _personalDataLink;
public Index(HttpClient client, IHtmlDocument manage, HtmlPageContext context) public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
: base(client, manage, context) : base(client, manage, context)
{ {
Assert.True(Context.UserAuthenticated);
_profileLink = HtmlAssert.HasLink("#profile", manage); _profileLink = HtmlAssert.HasLink("#profile", manage);
_changePasswordLink = HtmlAssert.HasLink("#change-password", manage); _changePasswordLink = HtmlAssert.HasLink("#change-password", manage);
_twoFactorLink = HtmlAssert.HasLink("#two-factor", manage); _twoFactorLink = HtmlAssert.HasLink("#two-factor", manage);
_personalDataLink = HtmlAssert.HasLink("#personal-data", manage); _personalDataLink = HtmlAssert.HasLink("#personal-data", manage);
} }
public async Task<TwoFactorAuthentication> ClickTwoFactorLinkAsync(bool twoFactorEnabled) public async Task<TwoFactorAuthentication> ClickTwoFactorLinkAsync()
{ {
var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href); var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href);
var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor); var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor);
return new TwoFactorAuthentication(Client, twoFactor, Context, twoFactorEnabled); return new TwoFactorAuthentication(Client, twoFactor, Context);
} }
} }
} }

View File

@ -8,17 +8,15 @@ using AngleSharp.Dom.Html;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage 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<IHtmlElement> _recoveryCodeElements; private readonly IEnumerable<IHtmlElement> _recoveryCodeElements;
public ShowRecoveryCodes(HttpClient client, IHtmlDocument showRecoveryCodes, HtmlPageContext context) public ShowRecoveryCodes(HttpClient client, IHtmlDocument showRecoveryCodes, DefaultUIContext context)
: base(client, showRecoveryCodes, context) : base(client, showRecoveryCodes, context)
{ {
_recoveryCodeElements = HtmlAssert.HasElements(".recovery-code", showRecoveryCodes); _recoveryCodeElements = HtmlAssert.HasElements(".recovery-code", showRecoveryCodes);
Context[RecoveryCodes] = string.Join(" ", Codes); Context.RecoveryCodes = Codes.ToArray();
} }
public IEnumerable<string> Codes => _recoveryCodeElements.Select(rc => rc.TextContent); public IEnumerable<string> Codes => _recoveryCodeElements.Select(rc => rc.TextContent);

View File

@ -8,16 +8,14 @@ using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
{ {
public class TwoFactorAuthentication : HtmlPage public class TwoFactorAuthentication : DefaultUIPage
{ {
private readonly bool _twoFactorEnabled;
private readonly IHtmlAnchorElement _enableAuthenticatorLink; 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) : base(client, twoFactor, context)
{ {
_twoFactorEnabled = twoFactorEnabled; if (!Context.TwoFactorEnabled)
if (!_twoFactorEnabled)
{ {
_enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor); _enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor);
} }
@ -25,7 +23,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
internal async Task<EnableAuthenticator> ClickEnableAuthenticatorLinkAsync() internal async Task<EnableAuthenticator> ClickEnableAuthenticatorLinkAsync()
{ {
Assert.False(_twoFactorEnabled); Assert.False(Context.TwoFactorEnabled);
var goToEnableAuthenticator = await Client.GetAsync(_enableAuthenticatorLink.Href); var goToEnableAuthenticator = await Client.GetAsync(_enableAuthenticatorLink.Href);
var enableAuthenticator = await ResponseAssert.IsHtmlDocumentAsync(goToEnableAuthenticator); var enableAuthenticator = await ResponseAssert.IsHtmlDocumentAsync(goToEnableAuthenticator);

View File

@ -9,11 +9,11 @@ using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
{ {
public class Register : HtmlPage public class Register : DefaultUIPage
{ {
private IHtmlFormElement _registerForm; private IHtmlFormElement _registerForm;
public Register(HttpClient client, IHtmlDocument register, HtmlPageContext context) public Register(HttpClient client, IHtmlDocument register, DefaultUIContext context)
: base(client, register, context) : base(client, register, context)
{ {
_registerForm = HtmlAssert.HasForm(register); _registerForm = HtmlAssert.HasForm(register);
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account
var indexResponse = await Client.GetAsync(registeredLocation); var indexResponse = await Client.GetAsync(registeredLocation);
var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse); var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse);
return new Index(Client, index, Context, authenticated: true); return new Index(Client, index, Context.WithAuthenticatedUser());
} }
} }
} }

View File

@ -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<ExternalLogin> SendNewUserNameAsync(string userName)
{
var externalLogin = await SendLoginForm(userName);
return new ExternalLogin(Client, externalLogin, Context);
}
public async Task<Index> SendExistingUserNameAsync(string userName)
{
var externalLogin = await SendLoginForm(userName);
return new Index(Client, externalLogin, Context.WithAuthenticatedUser());
}
private async Task<IHtmlDocument> SendLoginForm(string userName)
{
var contosoResponse = await Client.SendAsync(_loginForm, new Dictionary<string, string>
{
["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);
}
}
}
}

View File

@ -9,19 +9,20 @@ using Xunit;
namespace Microsoft.AspNetCore.Identity.FunctionalTests namespace Microsoft.AspNetCore.Identity.FunctionalTests
{ {
public class Index : HtmlPage public class Index : DefaultUIPage
{ {
private readonly bool _authenticated;
private readonly IHtmlAnchorElement _registerLink; private readonly IHtmlAnchorElement _registerLink;
private readonly IHtmlAnchorElement _loginLink; private readonly IHtmlAnchorElement _loginLink;
private readonly IHtmlAnchorElement _manageLink; private readonly IHtmlAnchorElement _manageLink;
public static readonly string Path = "/"; 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) : base(client, index, context)
{ {
_authenticated = authenticated; if (!Context.UserAuthenticated)
if (!_authenticated)
{ {
_registerLink = HtmlAssert.HasLink("#register", Document); _registerLink = HtmlAssert.HasLink("#register", Document);
_loginLink = HtmlAssert.HasLink("#login", Document); _loginLink = HtmlAssert.HasLink("#login", Document);
@ -32,17 +33,17 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
} }
} }
public static async Task<Index> CreateAsync(HttpClient client, bool authenticated = false) public static async Task<Index> CreateAsync(HttpClient client, DefaultUIContext context = null)
{ {
var goToIndex = await client.GetAsync("/"); var goToIndex = await client.GetAsync("/");
var index = await ResponseAssert.IsHtmlDocumentAsync(goToIndex); 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<Register> ClickRegisterLinkAsync() public async Task<Register> ClickRegisterLinkAsync()
{ {
Assert.False(_authenticated); Assert.False(Context.UserAuthenticated);
var goToRegister = await Client.GetAsync(_registerLink.Href); var goToRegister = await Client.GetAsync(_registerLink.Href);
var register = await ResponseAssert.IsHtmlDocumentAsync(goToRegister); var register = await ResponseAssert.IsHtmlDocumentAsync(goToRegister);
@ -52,7 +53,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
public async Task<Login> ClickLoginLinkAsync() public async Task<Login> ClickLoginLinkAsync()
{ {
Assert.False(_authenticated); Assert.False(Context.UserAuthenticated);
var goToLogin = await Client.GetAsync(_loginLink.Href); var goToLogin = await Client.GetAsync(_loginLink.Href);
var login = await ResponseAssert.IsHtmlDocumentAsync(goToLogin); var login = await ResponseAssert.IsHtmlDocumentAsync(goToLogin);
@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
internal async Task<Account.Manage.Index> ClickManageLinkAsync() internal async Task<Account.Manage.Index> ClickManageLinkAsync()
{ {
Assert.True(_authenticated); Assert.True(Context.UserAuthenticated);
var goToManage = await Client.GetAsync(_manageLink.Href); var goToManage = await Client.GetAsync(_manageLink.Href);
var manage = await ResponseAssert.IsHtmlDocumentAsync(goToManage); var manage = await ResponseAssert.IsHtmlDocumentAsync(goToManage);

View File

@ -21,5 +21,21 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
// Act & Assert // Act & Assert
await UserStories.RegisterNewUserAsync(client, userName, password); 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);
}
} }
} }

View File

@ -30,6 +30,34 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
return await login.LoginValidUserAsync(userName, password); return await login.LoginValidUserAsync(userName, password);
} }
internal static async Task<Index> 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<Index> 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<Index> LoginExistingUser2FaAsync(HttpClient client, string userName, string password, string twoFactorKey) internal static async Task<Index> LoginExistingUser2FaAsync(HttpClient client, string userName, string password, string twoFactorKey)
{ {
var index = await Index.CreateAsync(client); var index = await Index.CreateAsync(client);
@ -41,12 +69,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
return await login2Fa.Send2FACodeAsync(twoFactorKey); return await login2Fa.Send2FACodeAsync(twoFactorKey);
} }
internal static async Task<ShowRecoveryCodes> EnableTwoFactorAuthentication( internal static async Task<ShowRecoveryCodes> EnableTwoFactorAuthentication(Index index)
Index index,
bool twoFactorEnabled)
{ {
var manage = await index.ClickManageLinkAsync(); var manage = await index.ClickManageLinkAsync();
var twoFactor = await manage.ClickTwoFactorLinkAsync(twoFactorEnabled); var twoFactor = await manage.ClickTwoFactorLinkAsync();
var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync(); var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync();
return await enableAuthenticator.SendValidCodeAsync(); return await enableAuthenticator.SendValidCodeAsync();
} }
@ -63,7 +89,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
var login2Fa = await loginWithPassword.PasswordLoginValidUserWith2FaAsync(userName, password); var login2Fa = await loginWithPassword.PasswordLoginValidUserWith2FaAsync(userName, password);
var loginRecoveryCode = await login2Fa.ClickRecoveryCodeLinkAsync(); var loginRecoveryCode = await login2Fa.ClickRecoveryCodeLinkAsync();
return await loginRecoveryCode.SendRecoveryCodeAsync(recoveryCode); return await loginRecoveryCode.SendRecoveryCodeAsync(recoveryCode);
} }

View File

@ -0,0 +1,231 @@
// <auto-generated />
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<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -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");
}
}
}

View File

@ -1,11 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved. // <auto-generated />
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; 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 namespace Identity.DefaultUI.WebSite.Data.Migrations
{ {
@ -14,32 +16,36 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
{ {
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "1.0.0-rc3") .HasAnnotation("ProductVersion", "2.1.0-preview2-30103")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{ {
b.Property<string>("Id"); b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp") b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken(); .IsConcurrencyToken();
b.Property<string>("Name") b.Property<string>("Name")
.HasAnnotation("MaxLength", 256); .HasMaxLength(256);
b.Property<string>("NormalizedName") b.Property<string>("NormalizedName")
.HasAnnotation("MaxLength", 256); .HasMaxLength(256);
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("NormalizedName") b.HasIndex("NormalizedName")
.HasName("RoleNameIndex"); .IsUnique()
.HasName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles"); b.ToTable("AspNetRoles");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd(); .ValueGeneratedOnAdd();
@ -58,7 +64,58 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
b.ToTable("AspNetRoleClaims"); b.ToTable("AspNetRoleClaims");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("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<string>", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd(); .ValueGeneratedOnAdd();
@ -77,7 +134,7 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
b.ToTable("AspNetUserClaims"); b.ToTable("AspNetUserClaims");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{ {
b.Property<string>("LoginProvider"); b.Property<string>("LoginProvider");
@ -95,7 +152,7 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
b.ToTable("AspNetUserLogins"); b.ToTable("AspNetUserLogins");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{ {
b.Property<string>("UserId"); b.Property<string>("UserId");
@ -105,12 +162,10 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
b.HasIndex("RoleId"); b.HasIndex("RoleId");
b.HasIndex("UserId");
b.ToTable("AspNetUserRoles"); b.ToTable("AspNetUserRoles");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{ {
b.Property<string>("UserId"); b.Property<string>("UserId");
@ -125,91 +180,51 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
b.ToTable("AspNetUserTokens"); b.ToTable("AspNetUserTokens");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{ {
b.Property<string>("Id"); b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasAnnotation("MaxLength", 256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasAnnotation("MaxLength", 256);
b.Property<string>("NormalizedUserName")
.HasAnnotation("MaxLength", 256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole")
.WithMany("Claims")
.HasForeignKey("RoleId") .HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser")
.WithMany("Claims") .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser")
.WithMany("Logins") .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany("Users") .WithMany()
.HasForeignKey("RoleId") .HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser")
.WithMany("Roles") .WithMany()
.HasForeignKey("UserId") .HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
} }
} }
} }

View File

@ -39,4 +39,8 @@
<ProjectReference Include="..\..\..\src\UI\Microsoft.AspNetCore.Identity.UI.csproj" /> <ProjectReference Include="..\..\..\src\UI\Microsoft.AspNetCore.Identity.UI.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="$(MicrosoftEntityFrameworkCoreToolsDotNetPackageVersion)" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,37 @@
@page
@model LoginModel
@{
Layout = "_Layout";
ViewData["Title"] = "Contoso log-in";
}
<h2>@ViewData["Title"]</h2>
<div class="row">
<div class="col-md-4">
<section>
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Login"></label>
<input asp-for="Input.Login" class="form-control" />
<span asp-validation-for="Input.Login" class="text-danger"></span>
</div>
<div class="form-group">
<div class="checkbox">
<label asp-for="Input.RememberMe">
<input asp-for="Input.RememberMe" />
@Html.DisplayNameFor(m => m.Input.RememberMe)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-default">Log in</button>
</div>
</form>
</section>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

View File

@ -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<ContosoAuthenticationOptions> 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<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
else
{
var state = JsonConvert.DeserializeObject<IDictionary<string, string>>(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 ?? "~/");
}
}
}
}

View File

@ -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<ContosoAuthenticationOptions> configure) =>
builder.AddScheme<ContosoAuthenticationOptions, ContosoAuthenticationHandler>(
ContosoAuthenticationConstants.Scheme,
ContosoAuthenticationConstants.DisplayName,
configure);
}
}

View File

@ -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";
}
}

View File

@ -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<ContosoAuthenticationOptions>
{
public ContosoAuthenticationHandler(
IOptionsMonitor<ContosoAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> 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<string, string>()
{
["State"] = JsonConvert.SerializeObject(properties.Items),
[Options.ReturnUrlQueryParameter] = properties.RedirectUri
});
Response.Redirect(uri);
return Task.CompletedTask;
}
}
}

View File

@ -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";
}
}

View File

@ -1,6 +1,7 @@
{ {
"ConnectionStrings": { "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": { "Logging": {
"LogLevel": { "LogLevel": {