Skip to content

Commit c83c384

Browse files
authored
Support loading OIDC options from configuration (#42679) (#42771)
* Support loading OIDC options from configuration * Address feedback from review * Address review feedback * Support fallbacks for all options and override lists * Fix up Enum.Parse and default for Authority
1 parent 44e64ad commit c83c384

18 files changed

+503
-24
lines changed

AspNetCore.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,6 +1737,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Blazor.WebAssembl
17371737
EndProject
17381738
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RateLimitingSample", "src\Middleware\RateLimiting\samples\RateLimitingSample\RateLimitingSample.csproj", "{91C3C03E-EA56-4ABA-9E73-A3DA4C2833D9}"
17391739
EndProject
1740+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalOpenIdConnectSample", "src\Security\Authentication\OpenIdConnect\samples\MinimalOpenIdConnectSample\MinimalOpenIdConnectSample.csproj", "{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}"
1741+
EndProject
17401742
Global
17411743
GlobalSection(SolutionConfigurationPlatforms) = preSolution
17421744
Debug|Any CPU = Debug|Any CPU
@@ -10434,6 +10436,22 @@ Global
1043410436
{91C3C03E-EA56-4ABA-9E73-A3DA4C2833D9}.Release|x64.Build.0 = Release|Any CPU
1043510437
{91C3C03E-EA56-4ABA-9E73-A3DA4C2833D9}.Release|x86.ActiveCfg = Release|Any CPU
1043610438
{91C3C03E-EA56-4ABA-9E73-A3DA4C2833D9}.Release|x86.Build.0 = Release|Any CPU
10439+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10440+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
10441+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|arm64.ActiveCfg = Debug|Any CPU
10442+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|arm64.Build.0 = Debug|Any CPU
10443+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|x64.ActiveCfg = Debug|Any CPU
10444+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|x64.Build.0 = Debug|Any CPU
10445+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|x86.ActiveCfg = Debug|Any CPU
10446+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Debug|x86.Build.0 = Debug|Any CPU
10447+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
10448+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|Any CPU.Build.0 = Release|Any CPU
10449+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|arm64.ActiveCfg = Release|Any CPU
10450+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|arm64.Build.0 = Release|Any CPU
10451+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|x64.ActiveCfg = Release|Any CPU
10452+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|x64.Build.0 = Release|Any CPU
10453+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|x86.ActiveCfg = Release|Any CPU
10454+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF}.Release|x86.Build.0 = Release|Any CPU
1043710455
EndGlobalSection
1043810456
GlobalSection(SolutionProperties) = preSolution
1043910457
HideSolutionNode = FALSE
@@ -11292,6 +11310,7 @@ Global
1129211310
{0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
1129311311
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9} = {08D53E58-4AAE-40C4-8497-63EC8664F304}
1129411312
{91C3C03E-EA56-4ABA-9E73-A3DA4C2833D9} = {1D865E78-7A66-4CA9-92EE-2B350E45281F}
11313+
{07FDBE0D-B7A1-43DE-B120-F699C30E7CEF} = {E19E55A2-1562-47A7-8EA6-B51F2CA0CC4C}
1129511314
EndGlobalSection
1129611315
GlobalSection(ExtensibilityGlobals) = postSolution
1129711316
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,25 @@
99
"DefaultScheme": "ClaimedDetails",
1010
"Schemes": {
1111
"Bearer": {
12-
"Audiences": [
12+
"ValidAudiences": [
1313
"https://localhost:7259",
1414
"http://localhost:5259"
1515
],
16-
"ClaimsIssuer": "dotnet-user-jwts"
16+
"ValidIssuer": "dotnet-user-jwts"
1717
},
1818
"ClaimedDetails": {
19-
"Audiences": [
19+
"ValidAudiences": [
2020
"https://localhost:7259",
2121
"http://localhost:5259"
2222
],
23-
"ClaimsIssuer": "dotnet-user-jwts"
23+
"ValidIssuer": "dotnet-user-jwts"
2424
},
2525
"InvalidScheme": {
26-
"Audiences": [
26+
"ValidAudiences": [
2727
"https://localhost:7259",
2828
"http://localhost:5259"
2929
],
30-
"ClaimsIssuer": "invalid-issuer"
30+
"ValidIssuer": "invalid-issuer"
3131
}
3232
}
3333
}

src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Globalization;
45
using System.Linq;
5-
using System.Security.Cryptography;
66
using Microsoft.AspNetCore.Authentication.JwtBearer;
77
using Microsoft.Extensions.Configuration;
88
using Microsoft.Extensions.Options;
@@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Authentication;
1313
internal sealed class JwtBearerConfigureOptions : IConfigureNamedOptions<JwtBearerOptions>
1414
{
1515
private readonly IAuthenticationConfigurationProvider _authenticationConfigurationProvider;
16+
private static readonly Func<string, TimeSpan> _invariantTimeSpanParse = (string timespanString) => TimeSpan.Parse(timespanString, CultureInfo.InvariantCulture);
1617

1718
/// <summary>
1819
/// Initializes a new <see cref="JwtBearerConfigureOptions"/> given the configuration
@@ -39,26 +40,56 @@ public void Configure(string? name, JwtBearerOptions options)
3940
return;
4041
}
4142

42-
var issuer = configSection["ClaimsIssuer"];
43-
var audiences = configSection.GetSection("Audiences").GetChildren().Select(aud => aud.Value).ToArray();
43+
var issuer = configSection[nameof(TokenValidationParameters.ValidIssuer)];
44+
var issuers = configSection.GetSection(nameof(TokenValidationParameters.ValidIssuers)).GetChildren().Select(iss => iss.Value).ToList();
45+
if (issuer is not null)
46+
{
47+
issuers.Add(issuer);
48+
}
49+
var audience = configSection[nameof(TokenValidationParameters.ValidAudience)];
50+
var audiences = configSection.GetSection(nameof(TokenValidationParameters.ValidAudiences)).GetChildren().Select(aud => aud.Value).ToList();
51+
if (audience is not null)
52+
{
53+
audiences.Add(audience);
54+
}
55+
56+
options.Authority = configSection[nameof(options.Authority)] ?? options.Authority;
57+
options.BackchannelTimeout = StringHelpers.ParseValueOrDefault(configSection[nameof(options.BackchannelTimeout)], _invariantTimeSpanParse, options.BackchannelTimeout);
58+
options.Challenge = configSection[nameof(options.Challenge)] ?? options.Challenge;
59+
options.ForwardAuthenticate = configSection[nameof(options.ForwardAuthenticate)] ?? options.ForwardAuthenticate;
60+
options.ForwardChallenge = configSection[nameof(options.ForwardChallenge)] ?? options.ForwardChallenge;
61+
options.ForwardDefault = configSection[nameof(options.ForwardDefault)] ?? options.ForwardDefault;
62+
options.ForwardForbid = configSection[nameof(options.ForwardForbid)] ?? options.ForwardForbid;
63+
options.ForwardSignIn = configSection[nameof(options.ForwardSignIn)] ?? options.ForwardSignIn;
64+
options.ForwardSignOut = configSection[nameof(options.ForwardSignOut)] ?? options.ForwardSignOut;
65+
options.IncludeErrorDetails = StringHelpers.ParseValueOrDefault(configSection[nameof(options.IncludeErrorDetails)], bool.Parse, options.IncludeErrorDetails);
66+
options.MapInboundClaims = StringHelpers.ParseValueOrDefault( configSection[nameof(options.MapInboundClaims)], bool.Parse, options.MapInboundClaims);
67+
options.MetadataAddress = configSection[nameof(options.MetadataAddress)] ?? options.MetadataAddress;
68+
options.RefreshInterval = StringHelpers.ParseValueOrDefault(configSection[nameof(options.RefreshInterval)], _invariantTimeSpanParse, options.RefreshInterval);
69+
options.RefreshOnIssuerKeyNotFound = StringHelpers.ParseValueOrDefault(configSection[nameof(options.RefreshOnIssuerKeyNotFound)], bool.Parse, options.RefreshOnIssuerKeyNotFound);
70+
options.RequireHttpsMetadata = StringHelpers.ParseValueOrDefault(configSection[nameof(options.RequireHttpsMetadata)], bool.Parse, options.RequireHttpsMetadata);
71+
options.SaveToken = StringHelpers.ParseValueOrDefault(configSection[nameof(options.SaveToken)], bool.Parse, options.SaveToken);
4472
options.TokenValidationParameters = new()
4573
{
46-
ValidateIssuer = issuer is not null,
47-
ValidIssuers = new[] { issuer },
48-
ValidateAudience = audiences.Length > 0,
74+
ValidateIssuer = issuers.Count > 0,
75+
ValidIssuers = issuers,
76+
ValidateAudience = audiences.Count > 0,
4977
ValidAudiences = audiences,
5078
ValidateIssuerSigningKey = true,
51-
IssuerSigningKey = GetIssuerSigningKey(configSection, issuer),
79+
IssuerSigningKeys = GetIssuerSigningKeys(configSection, issuers),
5280
};
5381
}
5482

55-
private static SecurityKey GetIssuerSigningKey(IConfiguration configuration, string? issuer)
83+
private static IEnumerable<SecurityKey> GetIssuerSigningKeys(IConfiguration configuration, List<string?> issuers)
5684
{
57-
var jwtKeyMaterialSecret = configuration[$"{issuer}:KeyMaterial"];
58-
var jwtKeyMaterial = !string.IsNullOrEmpty(jwtKeyMaterialSecret)
59-
? Convert.FromBase64String(jwtKeyMaterialSecret)
60-
: RandomNumberGenerator.GetBytes(32);
61-
return new SymmetricSecurityKey(jwtKeyMaterial);
85+
foreach (var issuer in issuers)
86+
{
87+
var keyFromSecret = configuration[$"{issuer}:KeyMaterial"];
88+
if (!string.IsNullOrEmpty(keyFromSecret))
89+
{
90+
yield return new SymmetricSecurityKey(Convert.FromBase64String(keyFromSecret));
91+
}
92+
}
6293
}
6394

6495
/// <inheritdoc />

src/Security/Authentication/JwtBearer/src/Microsoft.AspNetCore.Authentication.JwtBearer.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
<Reference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" />
1414
</ItemGroup>
1515

16+
<ItemGroup>
17+
<Compile Include="$(SharedSourceRoot)StringHelpers.cs" LinkBase="Shared" />
18+
</ItemGroup>
19+
1620
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Reference Include="Microsoft.AspNetCore" />
11+
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
12+
<Reference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
using System.Security.Claims;
4+
5+
var builder = WebApplication.CreateBuilder(args);
6+
7+
builder.Services
8+
.AddAuthentication("OpenIdConnect")
9+
.AddCookie()
10+
.AddOpenIdConnect();
11+
builder.Services.AddAuthorization();
12+
13+
var app = builder.Build();
14+
15+
app.MapGet("/protected", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}!")
16+
.RequireAuthorization();
17+
18+
app.Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:2726",
7+
"sslPort": 44308
8+
}
9+
},
10+
"profiles": {
11+
"http": {
12+
"commandName": "Project",
13+
"dotnetRunMessages": true,
14+
"launchBrowser": true,
15+
"applicationUrl": "http://localhost:5204",
16+
"environmentVariables": {
17+
"ASPNETCORE_ENVIRONMENT": "Development"
18+
}
19+
},
20+
"https": {
21+
"commandName": "Project",
22+
"dotnetRunMessages": true,
23+
"launchBrowser": true,
24+
"applicationUrl": "https://localhost:7282;http://localhost:5204",
25+
"environmentVariables": {
26+
"ASPNETCORE_ENVIRONMENT": "Development"
27+
}
28+
},
29+
"IIS Express": {
30+
"commandName": "IISExpress",
31+
"launchBrowser": true,
32+
"environmentVariables": {
33+
"ASPNETCORE_ENVIRONMENT": "Development"
34+
}
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

src/Security/Authentication/OpenIdConnect/src/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@
1313
<Reference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" />
1414
</ItemGroup>
1515

16+
<ItemGroup>
17+
<Compile Include="$(SharedSourceRoot)StringHelpers.cs" LinkBase="Shared" />
18+
</ItemGroup>
19+
1620
</Project>

0 commit comments

Comments
 (0)