Skip to content

Commit f0e1d1e

Browse files
authored
Fix up reading auth schemes and setting default scheme (dotnet#42452)
* Fix up reading auth schemes and setting default scheme * Address feedback from peer review
1 parent a0513ea commit f0e1d1e

File tree

7 files changed

+129
-24
lines changed

7 files changed

+129
-24
lines changed

src/Http/Authentication.Core/src/AuthenticationService.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Authentication;
1414
public class AuthenticationService : IAuthenticationService
1515
{
1616
private HashSet<ClaimsPrincipal>? _transformCache;
17+
private const string defaultSchemesOptionsMsg = "The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions) or by setting the Authentication:DefaultScheme property in configuration.";
1718

1819
/// <summary>
1920
/// Constructor.
@@ -64,7 +65,7 @@ public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext cont
6465
scheme = defaultScheme?.Name;
6566
if (scheme == null)
6667
{
67-
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
68+
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. {defaultSchemesOptionsMsg}");
6869
}
6970
}
7071

@@ -112,7 +113,7 @@ public virtual async Task ChallengeAsync(HttpContext context, string? scheme, Au
112113
scheme = defaultChallengeScheme?.Name;
113114
if (scheme == null)
114115
{
115-
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
116+
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. {defaultSchemesOptionsMsg}");
116117
}
117118
}
118119

@@ -140,7 +141,7 @@ public virtual async Task ForbidAsync(HttpContext context, string? scheme, Authe
140141
scheme = defaultForbidScheme?.Name;
141142
if (scheme == null)
142143
{
143-
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
144+
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found. {defaultSchemesOptionsMsg}");
144145
}
145146
}
146147

@@ -186,7 +187,7 @@ public virtual async Task SignInAsync(HttpContext context, string? scheme, Claim
186187
scheme = defaultScheme?.Name;
187188
if (scheme == null)
188189
{
189-
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
190+
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found. {defaultSchemesOptionsMsg}");
190191
}
191192
}
192193

@@ -220,7 +221,7 @@ public virtual async Task SignOutAsync(HttpContext context, string? scheme, Auth
220221
scheme = defaultScheme?.Name;
221222
if (scheme == null)
222223
{
223-
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
224+
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found. {defaultSchemesOptionsMsg}");
224225
}
225226
}
226227

src/Security/Authentication/Core/src/AuthenticationConfigurationProviderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Authentication;
1010
/// </summary>
1111
public static class AuthenticationConfigurationProviderExtensions
1212
{
13-
private const string AuthenticationSchemesKey = "Authentication:Schemes";
13+
private const string AuthenticationSchemesKey = "Schemes";
1414

1515
/// <summary>
1616
/// Returns the specified <see cref="IConfiguration"/> object.

src/Security/Authentication/test/AuthenticationMiddlewareTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.Extensions.DependencyInjection;
1212
using Microsoft.Extensions.Hosting;
1313
using Microsoft.Extensions.Logging;
14+
using Microsoft.Extensions.Options;
1415
using Moq;
1516

1617
namespace Microsoft.AspNetCore.Authentication;
@@ -156,6 +157,11 @@ public async Task IAuthenticateResultFeature_SettingResultSetsUser()
156157
public async Task WebApplicationBuilder_RegistersAuthenticationMiddlewares()
157158
{
158159
var builder = WebApplication.CreateBuilder();
160+
builder.Configuration.AddInMemoryCollection(new[]
161+
{
162+
new KeyValuePair<string, string>("Authentication:Schemes:Bearer:ClaimsIssuer", "SomeIssuer"),
163+
new KeyValuePair<string, string>("Authentication:Schemes:Bearer:Audiences:0", "https://localhost:5001")
164+
});
159165
builder.Authentication.AddJwtBearer();
160166
await using var app = builder.Build();
161167

@@ -169,6 +175,10 @@ public async Task WebApplicationBuilder_RegistersAuthenticationMiddlewares()
169175
await app.StartAsync();
170176

171177
Assert.True(app.Properties.ContainsKey("__AuthenticationMiddlewareSet"));
178+
179+
var options = app.Services.GetService<IOptionsMonitor<JwtBearerOptions>>().Get(JwtBearerDefaults.AuthenticationScheme);
180+
Assert.Equal(new[] { "SomeIssuer" }, options.TokenValidationParameters.ValidIssuers);
181+
Assert.Equal(new[] { "https://localhost:5001" }, options.TokenValidationParameters.ValidAudiences);
172182
}
173183

174184
private HttpContext GetHttpContext(

src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ private static int Execute(IReporter reporter, string projectPath, bool force)
4545

4646
if (!force)
4747
{
48-
reporter.Output(Resources.ClearCommand_Permission);
48+
reporter.Output(Resources.FormatClearCommand_Permission(count, project));
4949
reporter.Output("[Y]es / [N]o");
5050
if (Console.ReadLine().Trim().ToUpperInvariant() != "Y")
5151
{

src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
1010
internal sealed record JwtAuthenticationSchemeSettings(string SchemeName, List<string> Audiences, string ClaimsIssuer)
1111
{
1212
private const string AuthenticationKey = "Authentication";
13+
private const string DefaultSchemeKey = "DefaultScheme";
1314
private const string SchemesKey = "Schemes";
1415

1516
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
@@ -35,7 +36,7 @@ public void Save(string filePath)
3536
{
3637
// If a scheme with the same name has already been registered, we
3738
// override with the latest token's options
38-
schemes[SchemeName] = settingsObject;
39+
schemes[SchemeName] = settingsObject;
3940
}
4041
else
4142
{
@@ -56,6 +57,15 @@ public void Save(string filePath)
5657
};
5758
}
5859

60+
// Set the DefaultScheme if it has not already been set
61+
// and only a single scheme has been configured thus far
62+
if (config[AuthenticationKey][DefaultSchemeKey] is null
63+
&& config[AuthenticationKey][SchemesKey] is JsonObject setSchemes
64+
&& setSchemes.Count == 1)
65+
{
66+
config[AuthenticationKey][DefaultSchemeKey] = SchemeName;
67+
}
68+
5969
using var writer = new FileStream(filePath, FileMode.Open, FileAccess.Write);
6070
JsonSerializer.Serialize(writer, config, _jsonSerializerOptions);
6171
}
@@ -70,6 +80,11 @@ public static void RemoveScheme(string filePath, string name)
7080
authentication[SchemesKey] is JsonObject schemes)
7181
{
7282
schemes.Remove(name);
83+
if (authentication[DefaultSchemeKey] is JsonValue defaultScheme
84+
&& defaultScheme.GetValue<string>() == name)
85+
{
86+
authentication.Remove(DefaultSchemeKey);
87+
}
7388
}
7489

7590
using var writer = new FileStream(filePath, FileMode.Create, FileAccess.Write);

src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class UserJwtsTestFixture : IDisposable
6262
}
6363
}";
6464

65-
public string CreateProject(bool hasSecret = true)
65+
public string CreateProject(bool hasSecret = true, string appSettingsContent = "{}")
6666
{
6767
var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "userjwtstest", Guid.NewGuid().ToString()));
6868
Directory.CreateDirectory(Path.Combine(projectPath.FullName, "Properties"));
@@ -81,7 +81,7 @@ public string CreateProject(bool hasSecret = true)
8181

8282
File.WriteAllText(
8383
Path.Combine(projectPath.FullName, "appsettings.Development.json"),
84-
"{}");
84+
appSettingsContent);
8585

8686
if (hasSecret)
8787
{

src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,61 @@ public void Create_CreatesSecretOnNoSecretInproject()
6767
}
6868

6969
[Fact]
70-
public void Create_WritesGeneratedTokenToDisk()
70+
public async Task Create_SetsDefaultSchemeIfNoOtherSchemesSet()
7171
{
7272
var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
73-
var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
73+
var appSettingsPath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
7474
var app = new Program(_console);
7575

7676
app.Run(new[] { "create", "--project", project });
7777
Assert.Contains("New JWT saved", _console.GetOutput());
78-
Assert.Contains("dotnet-user-jwts", File.ReadAllText(appsettings));
78+
79+
using FileStream openStream = File.OpenRead(appSettingsPath);
80+
var appSettingsFile = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
81+
82+
Assert.True(appSettingsFile.TryGetPropertyValue("Authentication", out var authentication));
83+
Assert.Equal("Bearer", authentication["DefaultScheme"].GetValue<string>());
84+
Assert.Equal("dotnet-user-jwts", authentication["Schemes"]["Bearer"]["ClaimsIssuer"].GetValue<string>());
85+
}
86+
87+
[Fact]
88+
public async Task Create_DoesNotOverrideDefaultSchemeIfAlreadySet()
89+
{
90+
var project = Path.Combine(_fixture.CreateProject(
91+
hasSecret: true,
92+
appSettingsContent: @"{ ""Authentication"": { ""DefaultScheme"": ""foobar"" } }"), "TestProject.csproj");
93+
var appSettingsPath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
94+
var app = new Program(_console);
95+
96+
app.Run(new[] { "create", "--project", project });
97+
Assert.Contains("New JWT saved", _console.GetOutput());
98+
99+
using FileStream openStream = File.OpenRead(appSettingsPath);
100+
var appSettingsFile = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
101+
102+
Assert.True(appSettingsFile.TryGetPropertyValue("Authentication", out var authentication));
103+
Assert.Equal("foobar", authentication["DefaultScheme"].GetValue<string>()); //foobar not Bearer
104+
Assert.Equal("dotnet-user-jwts", authentication["Schemes"]["Bearer"]["ClaimsIssuer"].GetValue<string>());
105+
}
106+
107+
[Fact]
108+
public async Task Create_DoesNotSetDefaultSchemeIfMultipleSchemesConfigured()
109+
{
110+
var project = Path.Combine(_fixture.CreateProject(
111+
hasSecret: true,
112+
appSettingsContent: @"{ ""Authentication"": { ""Schemes"": { ""foobar"" : { } } } }"), "TestProject.csproj");
113+
var appSettingsPath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
114+
var app = new Program(_console);
115+
116+
app.Run(new[] { "create", "--project", project });
117+
Assert.Contains("New JWT saved", _console.GetOutput());
118+
119+
using FileStream openStream = File.OpenRead(appSettingsPath);
120+
var appSettingsFile = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
121+
122+
Assert.True(appSettingsFile.TryGetPropertyValue("Authentication", out var authentication));
123+
Assert.Null(authentication["DefaultScheme"]); // Should not be set beause 2 schemes configured
124+
Assert.Equal("dotnet-user-jwts", authentication["Schemes"]["Bearer"]["ClaimsIssuer"].GetValue<string>());
79125
}
80126

81127
[Fact]
@@ -92,7 +138,6 @@ public void Print_ReturnsNothingForMissingToken()
92138
public void List_ReturnsIdForGeneratedToken()
93139
{
94140
var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
95-
var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
96141
var app = new Program(_console);
97142

98143
app.Run(new[] { "create", "--project", project, "--scheme", "MyCustomScheme" });
@@ -103,10 +148,10 @@ public void List_ReturnsIdForGeneratedToken()
103148
}
104149

105150
[Fact]
106-
public void Remove_RemovesGeneratedToken()
151+
public async Task Remove_RemovesGeneratedToken()
107152
{
108153
var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
109-
var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
154+
var appSettingsPath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
110155
var app = new Program(_console);
111156

112157
app.Run(new[] { "create", "--project", project });
@@ -115,16 +160,45 @@ public void Remove_RemovesGeneratedToken()
115160
app.Run(new[] { "create", "--project", project, "--scheme", "Scheme2" });
116161

117162
app.Run(new[] { "remove", id, "--project", project });
118-
var appsettingsContent = File.ReadAllText(appsettings);
119-
Assert.DoesNotContain("Bearer", appsettingsContent);
120-
Assert.Contains("Scheme2", appsettingsContent);
163+
164+
using FileStream openStream = File.OpenRead(appSettingsPath);
165+
var appSettingsFile = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
166+
167+
Assert.True(appSettingsFile.TryGetPropertyValue("Authentication", out var authentication));
168+
Assert.Null(authentication["Schemes"]["Bearer"]);
169+
Assert.NotNull(authentication["Schemes"]["Scheme2"]);
170+
Assert.Null(authentication["DefaultScheme"]);
171+
}
172+
173+
[Fact]
174+
public async Task Remove_DoesNotUnsetDefaultSchemeIfNoMatch()
175+
{
176+
var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
177+
var appSettingsPath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
178+
var app = new Program(_console);
179+
180+
app.Run(new[] { "create", "--project", project });
181+
_console.ClearOutput();
182+
app.Run(new[] { "create", "--project", project, "--scheme", "Scheme2" });
183+
var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'");
184+
var id = matches.SingleOrDefault().Groups[1].Value;
185+
186+
app.Run(new[] { "remove", id, "--project", project });
187+
188+
using FileStream openStream = File.OpenRead(appSettingsPath);
189+
var appSettingsFile = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
190+
191+
Assert.True(appSettingsFile.TryGetPropertyValue("Authentication", out var authentication));
192+
Assert.NotNull(authentication["Schemes"]["Bearer"]);
193+
Assert.Null(authentication["Schemes"]["Scheme2"]);
194+
Assert.NotNull(authentication["DefaultScheme"]); // We haven't removed the Bearer scheme so it's still the default
121195
}
122196

123197
[Fact]
124-
public void Clear_RemovesGeneratedTokens()
198+
public async Task Clear_RemovesGeneratedTokens()
125199
{
126200
var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
127-
var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
201+
var appSettingsPath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
128202
var app = new Program(_console);
129203

130204
app.Run(new[] { "create", "--project", project });
@@ -133,9 +207,14 @@ public void Clear_RemovesGeneratedTokens()
133207
Assert.Contains("New JWT saved", _console.GetOutput());
134208

135209
app.Run(new[] { "clear", "--project", project, "--force" });
136-
var appsettingsContent = File.ReadAllText(appsettings);
137-
Assert.DoesNotContain("Bearer", appsettingsContent);
138-
Assert.DoesNotContain("Scheme2", appsettingsContent);
210+
211+
using FileStream openStream = File.OpenRead(appSettingsPath);
212+
var appSettingsFile = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
213+
214+
Assert.True(appSettingsFile.TryGetPropertyValue("Authentication", out var authentication));
215+
Assert.Null(authentication["Schemes"]["Bearer"]);
216+
Assert.Null(authentication["Schemes"]["Scheme2"]);
217+
Assert.Null(authentication["DefaultScheme"]);
139218
}
140219

141220
[Fact]

0 commit comments

Comments
 (0)