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.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

View File

@ -9,7 +9,7 @@
<div class="row">
<div class="col-md-4">
<section>
<form method="post">
<form id="account" method="post">
<h4>Use a local account to log in.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
@ -61,7 +61,7 @@
}
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>
<p>
@foreach (var provider in Model.ExternalLogins)

View File

@ -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<HttpResponseMessage> SendAsync(
this HttpClient client,
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)
{
@ -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<IHtmlElement>(submitElement);
var submit = form.GetSubmission(submitButton);
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.
// 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<IdentityDbContext>(options =>
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
{
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;
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; }
}
}

View File

@ -7,13 +7,26 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
{
public class HtmlPageContext
{
private readonly IDictionary<string, string> _properties =
new Dictionary<string, string>();
private readonly IDictionary<string, object> _properties;
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.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

View File

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

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
{
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<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)
@ -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<HttpResponseMessage> SendLoginForm(string userName, string password)

View File

@ -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<LoginWithRecoveryCode> ClickRecoveryCodeLinkAsync()

View File

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

View File

@ -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<ShowRecoveryCodes> SendValidCodeAsync()
{
var authenticatorKey = _codeElement.TextContent.Replace(" ", "");
Context[AuthenticatorKey] = authenticatorKey;
Context.AuthenticatorKey = authenticatorKey;
var verificationCode = ComputeCode(authenticatorKey);
var sendCodeResponse = await Client.SendAsync(_sendCodeForm, new Dictionary<string, string>

View File

@ -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<TwoFactorAuthentication> ClickTwoFactorLinkAsync(bool twoFactorEnabled)
public async Task<TwoFactorAuthentication> 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);
}
}
}

View File

@ -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<IHtmlElement> _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<string> Codes => _recoveryCodeElements.Select(rc => rc.TextContent);

View File

@ -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<EnableAuthenticator> ClickEnableAuthenticatorLinkAsync()
{
Assert.False(_twoFactorEnabled);
Assert.False(Context.TwoFactorEnabled);
var goToEnableAuthenticator = await Client.GetAsync(_enableAuthenticatorLink.Href);
var enableAuthenticator = await ResponseAssert.IsHtmlDocumentAsync(goToEnableAuthenticator);

View File

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

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
{
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<Index> CreateAsync(HttpClient client, bool authenticated = false)
public static async Task<Index> 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<Register> 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<Login> 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<Account.Manage.Index> ClickManageLinkAsync()
{
Assert.True(_authenticated);
Assert.True(Context.UserAuthenticated);
var goToManage = await Client.GetAsync(_manageLink.Href);
var manage = await ResponseAssert.IsHtmlDocumentAsync(goToManage);

View File

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

View File

@ -30,6 +30,34 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
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)
{
var index = await Index.CreateAsync(client);
@ -41,12 +69,10 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
return await login2Fa.Send2FACodeAsync(twoFactorKey);
}
internal static async Task<ShowRecoveryCodes> EnableTwoFactorAuthentication(
Index index,
bool twoFactorEnabled)
internal static async Task<ShowRecoveryCodes> 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);
}

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.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// <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
{
@ -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<string>("Id");
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasAnnotation("MaxLength", 256);
.HasMaxLength(256);
b.Property<string>("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<string>", b =>
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
@ -58,7 +64,58 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
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")
.ValueGeneratedOnAdd();
@ -77,7 +134,7 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin<string>", b =>
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
@ -95,7 +152,7 @@ namespace Identity.DefaultUI.WebSite.Data.Migrations
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole<string>", b =>
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("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<string>", b =>
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("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<string>", b =>
{
b.Property<string>("Id");
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")
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.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")
.WithMany("Claims")
.WithMany()
.HasForeignKey("UserId")
.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")
.WithMany("Logins")
.WithMany()
.HasForeignKey("UserId")
.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")
.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<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" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="$(MicrosoftEntityFrameworkCoreToolsDotNetPackageVersion)" />
</ItemGroup>
</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": {
"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": {