| File: DataProtectorTokenProvider.cs | Web Access |
| Project: src\aspnetcore\src\Identity\Core\src\Microsoft.AspNetCore.Identity.csproj (Microsoft.AspNetCore.Identity) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Identity; /// <summary> /// Provides protection and validation of identity tokens. /// </summary> /// <typeparam name="TUser">The type used to represent a user.</typeparam> public class DataProtectorTokenProvider<TUser> : IUserTwoFactorTokenProvider<TUser> where TUser : class { /// <summary> /// Initializes a new instance of the <see cref="DataProtectorTokenProvider{TUser}"/> class. /// </summary> /// <param name="dataProtectionProvider">The system data protection provider.</param> /// <param name="options">The configured <see cref="DataProtectionTokenProviderOptions"/>.</param> /// <param name="logger">The logger used to log messages, warnings and errors.</param> public DataProtectorTokenProvider(IDataProtectionProvider dataProtectionProvider, IOptions<DataProtectionTokenProviderOptions> options, ILogger<DataProtectorTokenProvider<TUser>> logger) { ArgumentNullException.ThrowIfNull(dataProtectionProvider); Options = options?.Value ?? new DataProtectionTokenProviderOptions(); // Use the Name as the purpose which should usually be distinct from others Protector = dataProtectionProvider.CreateProtector(Name ?? "DataProtectorTokenProvider"); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// <summary> /// Gets the <see cref="DataProtectionTokenProviderOptions"/> for this instance. /// </summary> /// <value> /// The <see cref="DataProtectionTokenProviderOptions"/> for this instance. /// </value> protected DataProtectionTokenProviderOptions Options { get; private set; } /// <summary> /// Gets the <see cref="IDataProtector"/> for this instance. /// </summary> /// <value> /// The <see cref="IDataProtector"/> for this instance. /// </value> protected IDataProtector Protector { get; private set; } /// <summary> /// Gets the name of this instance. /// </summary> /// <value> /// The name of this instance. /// </value> public string Name { get { return Options.Name; } } /// <summary> /// Gets the <see cref="ILogger"/> used to log messages from the provider. /// </summary> /// <value> /// The <see cref="ILogger"/> used to log messages from the provider. /// </value> public ILogger<DataProtectorTokenProvider<TUser>> Logger { get; } /// <summary> /// Generates a protected token for the specified <paramref name="user"/> as an asynchronous operation. /// </summary> /// <param name="purpose">The purpose the token will be used for.</param> /// <param name="manager">The <see cref="UserManager{TUser}"/> to retrieve user properties from.</param> /// <param name="user">The <typeparamref name="TUser"/> the token will be generated from.</param> /// <returns>A <see cref="Task{TResult}"/> representing the generated token.</returns> public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user) { ArgumentNullException.ThrowIfNull(user); var ms = new MemoryStream(); var userId = await manager.GetUserIdAsync(user); using (var writer = ms.CreateWriter()) { writer.Write(DateTimeOffset.UtcNow); writer.Write(userId); writer.Write(purpose ?? ""); string? stamp = null; if (manager.SupportsUserSecurityStamp) { stamp = await manager.GetSecurityStampAsync(user); } writer.Write(stamp ?? ""); } var protectedBytes = Protector.Protect(ms.ToArray()); return Convert.ToBase64String(protectedBytes); } /// <summary> /// Validates the protected <paramref name="token"/> for the specified <paramref name="user"/> and <paramref name="purpose"/> as an asynchronous operation. /// </summary> /// <param name="purpose">The purpose the token was be used for.</param> /// <param name="token">The token to validate.</param> /// <param name="manager">The <see cref="UserManager{TUser}"/> to retrieve user properties from.</param> /// <param name="user">The <typeparamref name="TUser"/> the token was generated for.</param> /// <returns> /// A <see cref="Task{TResult}"/> that represents the result of the asynchronous validation, /// containing true if the token is valid, otherwise false. /// </returns> public virtual async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user) { try { var unprotectedData = Protector.Unprotect(Convert.FromBase64String(token)); var ms = new MemoryStream(unprotectedData); using (var reader = ms.CreateReader()) { var creationTime = reader.ReadDateTimeOffset(); var expirationTime = creationTime + Options.TokenLifespan; if (expirationTime < DateTimeOffset.UtcNow) { Logger.InvalidExpirationTime(); return false; } var userId = reader.ReadString(); var actualUserId = await manager.GetUserIdAsync(user); if (userId != actualUserId) { Logger.UserIdsNotEquals(); return false; } var purp = reader.ReadString(); if (!string.Equals(purp, purpose)) { Logger.PurposeNotEquals(purpose, purp); return false; } var stamp = reader.ReadString(); if (reader.PeekChar() != -1) { Logger.UnexpectedEndOfInput(); return false; } if (manager.SupportsUserSecurityStamp) { var isEqualsSecurityStamp = stamp == await manager.GetSecurityStampAsync(user); if (!isEqualsSecurityStamp) { Logger.SecurityStampNotEquals(); } return isEqualsSecurityStamp; } var stampIsEmpty = stamp == ""; if (!stampIsEmpty) { Logger.SecurityStampIsNotEmpty(); } return stampIsEmpty; } } // ReSharper disable once EmptyGeneralCatchClause catch { // Do not leak exception Logger.UnhandledException(); } return false; } /// <summary> /// Returns a <see cref="bool"/> indicating whether a token generated by this instance /// can be used as a Two Factor Authentication token as an asynchronous operation. /// </summary> /// <param name="manager">The <see cref="UserManager{TUser}"/> to retrieve user properties from.</param> /// <param name="user">The <typeparamref name="TUser"/> the token was generated for.</param> /// <returns> /// A <see cref="Task{TResult}"/> that represents the result of the asynchronous query, /// containing true if a token generated by this instance can be used as a Two Factor Authentication token, otherwise false. /// </returns> /// <remarks>This method will always return false for instances of <see cref="DataProtectorTokenProvider{TUser}"/>.</remarks> public virtual Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user) { return Task.FromResult(false); } } /// <summary> /// Utility extensions to streams /// </summary> internal static class StreamExtensions { internal static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true); public static BinaryReader CreateReader(this Stream stream) => new BinaryReader(stream, DefaultEncoding, true); public static BinaryWriter CreateWriter(this Stream stream) => new BinaryWriter(stream, DefaultEncoding, true); public static DateTimeOffset ReadDateTimeOffset(this BinaryReader reader) => new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero); public static void Write(this BinaryWriter writer, DateTimeOffset value) { writer.Write(value.UtcTicks); } }