/
DataProtectorTokenProvider.cs
213 lines (187 loc) · 8.52 KB
/
DataProtectorTokenProvider.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// 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);
}
}