Add support for Authentication Tokens to Identity

This commit is contained in:
Hao Kung 2016-03-07 13:53:54 -08:00
parent c8849685cf
commit 82863e9444
21 changed files with 566 additions and 114 deletions

View File

@ -172,6 +172,9 @@ namespace IdentitySample.Controllers
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
// Update any authentication tokens if login succeeded
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
@ -188,7 +191,7 @@ namespace IdentitySample.Controllers
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Email);
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
@ -217,6 +220,10 @@ namespace IdentitySample.Controllers
{
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);
// Update any authentication tokens as well
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
return RedirectToLocal(returnUrl);
}
}

View File

@ -135,6 +135,11 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
/// </summary>
public DbSet<IdentityUserRole<TKey>> UserRoles { get; set; }
/// <summary>
/// Gets or sets the <see cref="DbSet{TEntity}"/> of User tokens.
/// </summary>
public DbSet<IdentityUserToken<TKey>> UserTokens { get; set; }
/// <summary>
/// Gets or sets the <see cref="DbSet{TEntity}"/> of roles.
/// </summary>
@ -207,6 +212,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
b.HasKey(l => new { l.LoginProvider, l.ProviderKey });
b.ToTable("AspNetUserLogins");
});
builder.Entity<IdentityUserToken<TKey>>(b =>
{
b.HasKey(l => new { l.UserId, l.LoginProvider, l.Name });
b.ToTable("AspNetUserTokens");
});
}
}
}

View File

@ -0,0 +1,34 @@
// 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;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
/// <summary>
/// Represents an authentication token for a user.
/// </summary>
/// <typeparam name="TKey">The type of the primary key used for users.</typeparam>
public class IdentityUserToken<TKey> where TKey : IEquatable<TKey>
{
/// <summary>
/// Gets or sets the primary key of the user that the token belongs to.
/// </summary>
public virtual TKey UserId { get; set; }
/// <summary>
/// Gets or sets the LoginProvider this token is from.
/// </summary>
public virtual string LoginProvider { get; set; }
/// <summary>
/// Gets or sets the name of the token.
/// </summary>
public virtual string Name { get; set; }
/// <summary>
/// Gets or sets the token value.
/// </summary>
public virtual string Value { get; set; }
}
}

View File

@ -63,7 +63,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
IUserLockoutStore<TUser>,
IUserPhoneNumberStore<TUser>,
IQueryableUserStore<TUser>,
IUserTwoFactorStore<TUser>
IUserTwoFactorStore<TUser>,
IUserAuthenticationTokenStore<TUser>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TContext : DbContext
@ -523,6 +524,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
private DbSet<IdentityUserClaim<TKey>> UserClaims { get { return Context.Set<IdentityUserClaim<TKey>>(); } }
private DbSet<IdentityUserRole<TKey>> UserRoles { get { return Context.Set<IdentityUserRole<TKey>>(); } }
private DbSet<IdentityUserLogin<TKey>> UserLogins { get { return Context.Set<IdentityUserLogin<TKey>>(); } }
private DbSet<IdentityUserToken<TKey>> UserTokens { get { return Context.Set<IdentityUserToken<TKey>>(); } }
/// <summary>
/// Get the claims associated with the specified <paramref name="user"/> as an asynchronous operation.
@ -653,7 +655,6 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
LoginProvider = login.LoginProvider,
ProviderDisplayName = login.ProviderDisplayName
};
// TODO: fixup so we don't have to update both
UserLogins.Add(l);
return Task.FromResult(false);
}
@ -1196,5 +1197,69 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
}
return new List<TUser>();
}
private Task<IdentityUserToken<TKey>> FindToken(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
var userId = user.Id;
return UserTokens.SingleOrDefaultAsync(l => l.UserId.Equals(userId) && l.LoginProvider == loginProvider && l.Name == name, cancellationToken);
}
// <inheritdoc>
public virtual async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var token = await FindToken(user, loginProvider, name, cancellationToken);
if (token == null)
{
UserTokens.Add(new IdentityUserToken<TKey>
{
UserId = user.Id,
LoginProvider = loginProvider,
Name = name,
Value = value
});
}
else
{
token.Value = value;
}
}
public async Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = user.Id;
var entry = await UserTokens.SingleOrDefaultAsync(l => l.UserId.Equals(userId) && l.LoginProvider == loginProvider && l.Name == name, cancellationToken);
if (entry != null)
{
UserTokens.Remove(entry);
}
}
public async Task<string> GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var entry = await FindToken(user, loginProvider, name, cancellationToken);
return entry?.Value;
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Identity
/// Provides protection and validation of identity tokens.
/// </summary>
/// <typeparam name="TUser">The type used to represent a user.</typeparam>
public class DataProtectorTokenProvider<TUser> : IUserTokenProvider<TUser> where TUser : class
public class DataProtectorTokenProvider<TUser> : IUserTwoFactorTokenProvider<TUser> where TUser : class
{
/// <summary>
/// Initializes a new instance of the <see cref="DataProtectorTokenProvider{TUser}"/> class.

View File

@ -1,7 +1,9 @@
// 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.Security.Claims;
using Microsoft.AspNetCore.Authentication;
namespace Microsoft.AspNetCore.Identity
{
@ -13,20 +15,22 @@ namespace Microsoft.AspNetCore.Identity
/// <summary>
/// Creates a new instance of <see cref="ExternalLoginInfo"/>
/// </summary>
/// <param name="externalPrincipal">The <see cref="ClaimsPrincipal"/> to associate with this login.</param>
/// <param name="principal">The <see cref="ClaimsPrincipal"/> to associate with this login.</param>
/// <param name="loginProvider">The provider associated with this login information.</param>
/// <param name="providerKey">The unique identifier for this user provided by the login provider.</param>
/// <param name="displayName">The display name for this user provided by the login provider.</param>
public ExternalLoginInfo(ClaimsPrincipal externalPrincipal, string loginProvider, string providerKey,
public ExternalLoginInfo(ClaimsPrincipal principal, string loginProvider, string providerKey,
string displayName) : base(loginProvider, providerKey, displayName)
{
ExternalPrincipal = externalPrincipal;
Principal = principal;
}
/// <summary>
/// Gets or sets the <see cref="ClaimsPrincipal"/> associated with this login.
/// </summary>
/// <value>The <see cref="ClaimsPrincipal"/> associated with this login.</value>
public ClaimsPrincipal ExternalPrincipal { get; set; }
public ClaimsPrincipal Principal { get; set; }
public IEnumerable<AuthenticationToken> AuthenticationTokens { get; set; }
}
}

View File

@ -0,0 +1,45 @@
// 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.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Provides an abstraction to store a user's authentication tokens.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public interface IUserAuthenticationTokenStore<TUser> : IUserStore<TUser> where TUser : class
{
/// <summary>
/// Sets the token value for a particular user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="loginProvider">The authentication provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="value">The value of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken);
/// <summary>
/// Deletes a token for a user.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="loginProvider">The authentication provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken);
/// <summary>
/// Returns the token value.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="loginProvider">The authentication provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task<string> GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken);
}
}

View File

@ -31,10 +31,7 @@ namespace Microsoft.AspNetCore.Identity
/// <param name="loginProvider">The login provide whose information should be removed.</param>
/// <param name="providerKey">The key given by the external login provider for the specified user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that contains a flag the result of the asynchronous removing operation. The flag will be true if
/// the login information was existed and removed, otherwise false.
/// </returns>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken);
/// <summary>

View File

@ -6,10 +6,10 @@ using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Provides an abstraction for token generators.
/// Provides an abstraction for two factor token generators.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public interface IUserTokenProvider<TUser> where TUser : class
public interface IUserTwoFactorTokenProvider<TUser> where TUser : class
{
/// <summary>
/// Generates a token for the specified <paramref name="user"/> and <paramref name="purpose"/>.
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Identity
/// and validated it with the same purpose a token with the purpose of TOTP would not pass the heck even if it was
/// for the same user.
///
/// Implementations of <see cref="IUserTokenProvider{TUser}"/> should validate that purpose is not null or empty to
/// Implementations of <see cref="IUserTwoFactorTokenProvider{TUser}"/> should validate that purpose is not null or empty to
/// help with token separation.
/// </remarks>
Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user);

View File

@ -133,11 +133,11 @@ namespace Microsoft.AspNetCore.Identity
/// Adds a token provider for the <seealso cref="UserType"/>.
/// </summary>
/// <param name="providerName">The name of the provider to add.</param>
/// <param name="provider">The type of the <see cref="IUserTokenProvider{TUser}"/> to add.</param>
/// <param name="provider">The type of the <see cref="IUserTwoFactorTokenProvider{TUser}"/> to add.</param>
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
public virtual IdentityBuilder AddTokenProvider(string providerName, Type provider)
{
if (!typeof(IUserTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo()))
if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo()))
{
throw new InvalidOperationException(Resources.FormatInvalidManagerType(provider.Name, "IUserTokenProvider", UserType.Name));
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
@ -460,8 +461,43 @@ namespace Microsoft.AspNetCore.Identity
{
return null;
}
// REVIEW: fix this wrap
return new ExternalLoginInfo(auth.Principal, provider, providerKey, new AuthenticationDescription(auth.Description).DisplayName);
return new ExternalLoginInfo(auth.Principal, provider, providerKey, new AuthenticationDescription(auth.Description).DisplayName)
{
AuthenticationTokens = new AuthenticationProperties(auth.Properties).GetTokens()
};
}
/// <summary>
/// Stores any authentication tokens found in the external authentication cookie into the associated user.
/// </summary>
/// <param name="externalLogin">The information from the external login provider.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the operation.</returns>
public virtual async Task<IdentityResult> UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin)
{
if (externalLogin == null)
{
throw new ArgumentNullException(nameof(externalLogin));
}
if (externalLogin.AuthenticationTokens != null && externalLogin.AuthenticationTokens.Any())
{
var user = await UserManager.FindByLoginAsync(externalLogin.LoginProvider, externalLogin.ProviderKey);
if (user == null)
{
return IdentityResult.Failed();
}
foreach (var token in externalLogin.AuthenticationTokens)
{
var result = await UserManager.SetAuthenticationTokenAsync(user, externalLogin.LoginProvider, token.Name, token.Value);
if (!result.Succeeded)
{
return result;
}
}
}
return IdentityResult.Success;
}
/// <summary>

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Identity
/// Represents a token provider that generates time based codes using the user's security stamp.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public abstract class TotpSecurityStampBasedTokenProvider<TUser> : IUserTokenProvider<TUser>
public abstract class TotpSecurityStampBasedTokenProvider<TUser> : IUserTwoFactorTokenProvider<TUser>
where TUser : class
{
/// <summary>
@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Identity
/// and validated it with the same purpose a token with the purpose of TOTP would not pass the check even if it was
/// for the same user.
///
/// Implementations of <see cref="IUserTokenProvider{TUser}"/> should validate that purpose is not null or empty to
/// Implementations of <see cref="IUserTwoFactorTokenProvider{TUser}"/> should validate that purpose is not null or empty to
/// help with token separation.
/// </remarks>
public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)

View File

@ -26,8 +26,8 @@ namespace Microsoft.AspNetCore.Identity
protected const string ResetPasswordTokenPurpose = "ResetPassword";
protected const string ConfirmEmailTokenPurpose = "EmailConfirmation";
private readonly Dictionary<string, IUserTokenProvider<TUser>> _tokenProviders =
new Dictionary<string, IUserTokenProvider<TUser>>();
private readonly Dictionary<string, IUserTwoFactorTokenProvider<TUser>> _tokenProviders =
new Dictionary<string, IUserTwoFactorTokenProvider<TUser>>();
private TimeSpan _defaultLockout = TimeSpan.Zero;
private bool _disposed;
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Identity
_context = services.GetService<IHttpContextAccessor>()?.HttpContext;
foreach (var providerName in Options.Tokens.ProviderMap.Keys)
{
var provider = services.GetRequiredService(Options.Tokens.ProviderMap[providerName].ProviderType) as IUserTokenProvider<TUser>;
var provider = services.GetRequiredService(Options.Tokens.ProviderMap[providerName].ProviderType) as IUserTwoFactorTokenProvider<TUser>;
if (provider != null)
{
RegisterTokenProvider(providerName, provider);
@ -122,6 +122,21 @@ namespace Microsoft.AspNetCore.Identity
internal IdentityOptions Options { get; set; }
/// <summary>
/// Gets a flag indicating whether the backing user store supports authentication tokens.
/// </summary>
/// <value>
/// true if the backing user store supports authentication tokens, otherwise false.
/// </value>
public virtual bool SupportsUserAuthenticationTokens
{
get
{
ThrowIfDisposed();
return Store is IUserAuthenticationTokenStore<TUser>;
}
}
/// <summary>
/// Gets a flag indicating whether the backing user store supports two factor authentication.
/// </summary>
@ -1601,7 +1616,7 @@ namespace Microsoft.AspNetCore.Identity
/// </summary>
/// <param name="providerName">The name of the provider to register.</param>
/// <param name="provider">The provider to register.</param>
public virtual void RegisterTokenProvider(string providerName, IUserTokenProvider<TUser> provider)
public virtual void RegisterTokenProvider(string providerName, IUserTwoFactorTokenProvider<TUser> provider)
{
ThrowIfDisposed();
if (provider == null)
@ -1953,6 +1968,92 @@ namespace Microsoft.AspNetCore.Identity
return store.GetUsersInRoleAsync(NormalizeKey(roleName), CancellationToken);
}
/// <summary>
/// Returns an authentication token for a user.
/// </summary>
/// <param name="user"></param>
/// <param name="loginProvider">The authentication scheme for the provider the token is associated with.</param>
/// <param name="tokenName">The name of the token.</param>
/// <returns></returns>
public virtual Task<string> GetAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName)
{
ThrowIfDisposed();
var store = GetTokenStore();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (loginProvider == null)
{
throw new ArgumentNullException(nameof(loginProvider));
}
if (tokenName == null)
{
throw new ArgumentNullException(nameof(tokenName));
}
return store.GetTokenAsync(user, loginProvider, tokenName, CancellationToken);
}
/// <summary>
/// Sets an authentication token for a user.
/// </summary>
/// <param name="user"></param>
/// <param name="loginProvider">The authentication scheme for the provider the token is associated with.</param>
/// <param name="tokenName">The name of the token.</param>
/// <param name="tokenValue">The value of the token.</param>
/// <returns></returns>
public virtual async Task<IdentityResult> SetAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName, string tokenValue)
{
ThrowIfDisposed();
var store = GetTokenStore();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (loginProvider == null)
{
throw new ArgumentNullException(nameof(loginProvider));
}
if (tokenName == null)
{
throw new ArgumentNullException(nameof(tokenName));
}
// REVIEW: should updating any tokens affect the security stamp?
await store.SetTokenAsync(user, loginProvider, tokenName, tokenValue, CancellationToken);
return await UpdateUserAsync(user);
}
/// <summary>
/// Remove an authentication token for a user.
/// </summary>
/// <param name="user"></param>
/// <param name="loginProvider">The authentication scheme for the provider the token is associated with.</param>
/// <param name="tokenName">The name of the token.</param>
/// <returns>Whether a token was removed.</returns>
public virtual async Task<IdentityResult> RemoveAuthenticationTokenAsync(TUser user, string loginProvider, string tokenName)
{
ThrowIfDisposed();
var store = GetTokenStore();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (loginProvider == null)
{
throw new ArgumentNullException(nameof(loginProvider));
}
if (tokenName == null)
{
throw new ArgumentNullException(nameof(tokenName));
}
await store.RemoveTokenAsync(user, loginProvider, tokenName, CancellationToken);
return await UpdateUserAsync(user);
}
/// <summary>
/// Releases the unmanaged resources used by the role manager and optionally releases the managed resources.
/// </summary>
@ -1966,7 +2067,6 @@ namespace Microsoft.AspNetCore.Identity
}
}
// IUserFactorStore methods
internal IUserTwoFactorStore<TUser> GetUserTwoFactorStore()
{
var cast = Store as IUserTwoFactorStore<TUser>;
@ -1977,7 +2077,6 @@ namespace Microsoft.AspNetCore.Identity
return cast;
}
// IUserLockoutStore methods
internal IUserLockoutStore<TUser> GetUserLockoutStore()
{
var cast = Store as IUserLockoutStore<TUser>;
@ -1988,7 +2087,6 @@ namespace Microsoft.AspNetCore.Identity
return cast;
}
// IUserEmailStore methods
internal IUserEmailStore<TUser> GetEmailStore(bool throwOnFail = true)
{
var cast = Store as IUserEmailStore<TUser>;
@ -1999,7 +2097,6 @@ namespace Microsoft.AspNetCore.Identity
return cast;
}
// IUserPhoneNumberStore methods
internal IUserPhoneNumberStore<TUser> GetPhoneNumberStore()
{
var cast = Store as IUserPhoneNumberStore<TUser>;
@ -2010,7 +2107,6 @@ namespace Microsoft.AspNetCore.Identity
return cast;
}
// Two factor APIS
internal async Task<byte[]> CreateSecurityTokenAsync(TUser user)
{
return Encoding.Unicode.GetBytes(await GetSecurityStampAsync(user));
@ -2137,11 +2233,6 @@ namespace Microsoft.AspNetCore.Identity
return IdentityResult.Success;
}
/// <summary>
/// Validate user and update. Called by other UserManager methods
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
private async Task<IdentityResult> UpdateUserAsync(TUser user)
{
var result = await ValidateUserInternal(user);
@ -2154,7 +2245,16 @@ namespace Microsoft.AspNetCore.Identity
return await Store.UpdateAsync(user, CancellationToken);
}
// IUserPasswordStore methods
private IUserAuthenticationTokenStore<TUser> GetTokenStore()
{
var cast = Store as IUserAuthenticationTokenStore<TUser>;
if (cast == null)
{
throw new NotSupportedException("Resources.StoreNotIUserAuthenticationTokenStore");
}
return cast;
}
private IUserPasswordStore<TUser> GetPasswordStore()
{
var cast = Store as IUserPasswordStore<TUser>;

View File

@ -112,6 +112,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
Assert.True(VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId"));
Assert.True(VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue"));
Assert.True(VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName"));
Assert.True(VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value"));
VerifyIndex(db, "AspNetRoles", "RoleNameIndex");
VerifyIndex(db, "AspNetUsers", "UserNameIndex");

View File

@ -0,0 +1,117 @@
// 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.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity.Test;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Identity.InMemory.Test
{
public class ControllerTest
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task VerifyAccountControllerSignIn(bool isPersistent)
{
var context = new Mock<HttpContext>();
var auth = new Mock<AuthenticationManager>();
context.Setup(c => c.Authentication).Returns(auth.Object).Verifiable();
auth.Setup(a => a.SignInAsync(new IdentityCookieOptions().ApplicationCookieAuthenticationScheme,
It.IsAny<ClaimsPrincipal>(),
It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
// REVIEW: is persistant mocking broken
//It.Is<AuthenticationProperties>(v => v.IsPersistent == isPersistent))).Returns(Task.FromResult(0)).Verifiable();
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(context.Object);
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(contextAccessor.Object);
services.AddIdentity<TestUser, TestRole>();
services.AddSingleton<IUserStore<TestUser>, InMemoryStore<TestUser, TestRole>>();
services.AddSingleton<IRoleStore<TestRole>, InMemoryStore<TestUser, TestRole>>();
var app = new ApplicationBuilder(services.BuildServiceProvider());
app.UseCookieAuthentication();
// Act
var user = new TestUser
{
UserName = "Yolo"
};
const string password = "Yol0Sw@g!";
var userManager = app.ApplicationServices.GetRequiredService<UserManager<TestUser>>();
var signInManager = app.ApplicationServices.GetRequiredService<SignInManager<TestUser>>();
IdentityResultAssert.IsSuccess(await userManager.CreateAsync(user, password));
var result = await signInManager.PasswordSignInAsync(user, password, isPersistent, false);
// Assert
Assert.True(result.Succeeded);
context.VerifyAll();
auth.VerifyAll();
contextAccessor.VerifyAll();
}
[Fact]
public async Task VerifyAccountControllerExternalLoginWithTokensFlow()
{
// Setup the external cookie like it would look from a real OAuth2
var externalId = "<externalId>";
var authScheme = "<authScheme>";
var externalIdentity = new ClaimsIdentity();
externalIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, externalId));
var externalPrincipal = new ClaimsPrincipal(externalIdentity);
var externalLogin = new ExternalLoginInfo(externalPrincipal, authScheme, externalId, "displayname")
{
AuthenticationTokens = new[] {
new AuthenticationToken { Name = "refresh_token", Value = "refresh" },
new AuthenticationToken { Name = "access_token", Value = "access" }
}
};
var auth = new Mock<AuthenticationManager>();
auth.Setup(a => a.AuthenticateAsync(It.IsAny<AuthenticateContext>())).Returns(Task.FromResult(0));
var context = new Mock<HttpContext>();
context.Setup(c => c.Authentication).Returns(auth.Object).Verifiable();
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(context.Object);
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(contextAccessor.Object);
services.AddIdentity<TestUser, TestRole>();
services.AddSingleton<IUserStore<TestUser>, InMemoryStore<TestUser, TestRole>>();
services.AddSingleton<IRoleStore<TestRole>, InMemoryStore<TestUser, TestRole>>();
var app = new ApplicationBuilder(services.BuildServiceProvider());
app.UseCookieAuthentication();
// Act
var user = new TestUser
{
UserName = "Yolo"
};
var userManager = app.ApplicationServices.GetRequiredService<UserManager<TestUser>>();
var signInManager = app.ApplicationServices.GetRequiredService<SignInManager<TestUser>>();
IdentityResultAssert.IsSuccess(await userManager.CreateAsync(user));
IdentityResultAssert.IsSuccess(await userManager.AddLoginAsync(user, new UserLoginInfo(authScheme, externalId, "whatever")));
IdentityResultAssert.IsSuccess(await signInManager.UpdateExternalAuthenticationTokensAsync(externalLogin));
Assert.Equal("refresh", await userManager.GetAuthenticationTokenAsync(user, authScheme, "refresh_token"));
Assert.Equal("access", await userManager.GetAuthenticationTokenAsync(user, authScheme, "access_token"));
}
}
}

View File

@ -1,66 +0,0 @@
// 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.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder.Internal;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity.Test;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Identity.InMemory.Test
{
public class HttpSignInTest
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task VerifyAccountControllerSignIn(bool isPersistent)
{
var context = new Mock<HttpContext>();
var auth = new Mock<AuthenticationManager>();
context.Setup(c => c.Authentication).Returns(auth.Object).Verifiable();
auth.Setup(a => a.SignInAsync(new IdentityCookieOptions().ApplicationCookieAuthenticationScheme,
It.IsAny<ClaimsPrincipal>(),
It.IsAny<AuthenticationProperties>())).Returns(Task.FromResult(0)).Verifiable();
// REVIEW: is persistant mocking broken
//It.Is<AuthenticationProperties>(v => v.IsPersistent == isPersistent))).Returns(Task.FromResult(0)).Verifiable();
var contextAccessor = new Mock<IHttpContextAccessor>();
contextAccessor.Setup(a => a.HttpContext).Returns(context.Object);
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(contextAccessor.Object);
services.AddIdentity<TestUser, TestRole>();
services.AddSingleton<IUserStore<TestUser>, InMemoryStore<TestUser, TestRole>>();
services.AddSingleton<IRoleStore<TestRole>, InMemoryStore<TestUser, TestRole>>();
var app = new ApplicationBuilder(services.BuildServiceProvider());
app.UseCookieAuthentication();
// Act
var user = new TestUser
{
UserName = "Yolo"
};
const string password = "Yol0Sw@g!";
var userManager = app.ApplicationServices.GetRequiredService<UserManager<TestUser>>();
var signInManager = app.ApplicationServices.GetRequiredService<SignInManager<TestUser>>();
IdentityResultAssert.IsSuccess(await userManager.CreateAsync(user, password));
var result = await signInManager.PasswordSignInAsync(user, password, isPersistent, false);
// Assert
Assert.True(result.Succeeded);
context.VerifyAll();
auth.VerifyAll();
contextAccessor.VerifyAll();
}
}
}

View File

@ -23,7 +23,8 @@ namespace Microsoft.AspNetCore.Identity.InMemory
IQueryableUserStore<TUser>,
IUserTwoFactorStore<TUser>,
IQueryableRoleStore<TRole>,
IRoleClaimStore<TRole>
IRoleClaimStore<TRole>,
IUserAuthenticationTokenStore<TUser>
where TRole : TestRole
where TUser : TestUser
{
@ -499,6 +500,54 @@ namespace Microsoft.AspNetCore.Identity.InMemory
return Task.FromResult(0);
}
public Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
{
var tokenEntity =
user.Tokens.SingleOrDefault(
l =>
l.TokenName == name && l.LoginProvider == loginProvider &&
l.UserId == user.Id);
if (tokenEntity != null)
{
tokenEntity.TokenValue = value;
}
else
{
user.Tokens.Add(new TestUserToken
{
UserId = user.Id,
LoginProvider = loginProvider,
TokenName = name,
TokenValue = value
});
}
return Task.FromResult(0);
}
public Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
var tokenEntity =
user.Tokens.SingleOrDefault(
l =>
l.TokenName == name && l.LoginProvider == loginProvider &&
l.UserId == user.Id);
if (tokenEntity != null)
{
user.Tokens.Remove(tokenEntity);
}
return Task.FromResult(0);
}
public Task<string> GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
var tokenEntity =
user.Tokens.SingleOrDefault(
l =>
l.TokenName == name && l.LoginProvider == loginProvider &&
l.UserId == user.Id);
return Task.FromResult(tokenEntity?.TokenValue);
}
public IQueryable<TRole> Roles
{
get { return _roles.Values.AsQueryable(); }

View File

@ -1133,7 +1133,7 @@ namespace Microsoft.AspNetCore.Identity.Test
}
}
private class NoOpTokenProvider : IUserTokenProvider<TestUser>
private class NoOpTokenProvider : IUserTwoFactorTokenProvider<TestUser>
{
public string Name { get; } = "Noop";

View File

@ -89,17 +89,9 @@ namespace Microsoft.AspNetCore.Identity.Test
/// </summary>
public virtual int AccessFailedCount { get; set; }
/// <summary>
/// Navigation property for users in the role
/// </summary>
public virtual ICollection<TestUserRole<TKey>> Roles { get; private set; } = new List<TestUserRole<TKey>>();
/// <summary>
/// Navigation property for users claims
/// </summary>
public virtual ICollection<TestUserClaim<TKey>> Claims { get; private set; } = new List<TestUserClaim<TKey>>();
/// <summary>
/// Navigation property for users logins
/// </summary>
public virtual ICollection<TestUserLogin<TKey>> Logins { get; private set; } = new List<TestUserLogin<TKey>>();
public virtual ICollection<TestUserToken<TKey>> Tokens { get; private set; } = new List<TestUserToken<TKey>>();
}
}

View File

@ -0,0 +1,36 @@
// 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;
namespace Microsoft.AspNetCore.Identity.Test
{
public class TestUserToken : TestUserToken<string> { }
/// <summary>
/// Entity type for a user's login (i.e. facebook, google)
/// </summary>
/// <typeparam name="TKey"></typeparam>
public class TestUserToken<TKey> where TKey : IEquatable<TKey>
{
/// <summary>
/// The login provider for the login (i.e. facebook, google)
/// </summary>
public virtual string LoginProvider { get; set; }
/// <summary>
/// Key representing the login for the provider
/// </summary>
public virtual string TokenName { get; set; }
/// <summary>
/// Display name for the login
/// </summary>
public virtual string TokenValue { get; set; }
/// <summary>
/// User Id for the user who owns this login
/// </summary>
public virtual TKey UserId { get; set; }
}
}

View File

@ -815,7 +815,7 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.False(await manager.IsEmailConfirmedAsync(user));
}
private class StaticTokenProvider : IUserTokenProvider<TUser>
private class StaticTokenProvider : IUserTwoFactorTokenProvider<TUser>
{
public async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
{
@ -1909,6 +1909,30 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.True(!factors.Any());
}
[Fact]
public async Task CanGetSetUpdateAndRemoveUserToken()
{
if (ShouldSkipDbTests())
{
return;
}
var manager = CreateManager();
var user = CreateTestUser();
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
Assert.Null(await manager.GetAuthenticationTokenAsync(user, "provider", "name"));
IdentityResultAssert.IsSuccess(await manager.SetAuthenticationTokenAsync(user, "provider", "name", "value"));
Assert.Equal("value", await manager.GetAuthenticationTokenAsync(user, "provider", "name"));
IdentityResultAssert.IsSuccess(await manager.SetAuthenticationTokenAsync(user, "provider", "name", "value2"));
Assert.Equal("value2", await manager.GetAuthenticationTokenAsync(user, "provider", "name"));
IdentityResultAssert.IsSuccess(await manager.RemoveAuthenticationTokenAsync(user, "whatevs", "name"));
Assert.Equal("value2", await manager.GetAuthenticationTokenAsync(user, "provider", "name"));
IdentityResultAssert.IsSuccess(await manager.RemoveAuthenticationTokenAsync(user, "provider", "name"));
Assert.Null(await manager.GetAuthenticationTokenAsync(user, "provider", "name"));
}
[Fact]
public async Task CanGetValidTwoFactor()
{