Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SymmetricSecurityKey in Boilerplate jwt tokens (#10259) #10260

Merged
merged 4 commits into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .github/workflows/admin-sample.cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,6 @@ jobs:
with:
name: server-bundle

- name: Delete DataProtectionCertificate.pfx
run: |
rm DataProtectionCertificate.pfx

- name: Extract identity certificate from env
uses: timheuer/[email protected]
with:
fileDir: './'
fileName: 'DataProtectionCertificate.pfx'
encodedString: ${{ secrets.API_DATA_PROTECTION_CERTIFICATE_FILE_BASE64 }}

- name: Retrieve AppleAuthKey.p8
run: echo "${{ secrets.APPSTORE_API_KEY_PRIVATE_KEY_ADMIN }}" > AppleAuthKey.p8

Expand Down
11 changes: 0 additions & 11 deletions .github/workflows/sales-module-demo.cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,6 @@ jobs:
with:
name: server-bundle

- name: Delete DataProtectionCertificate.pfx
run: |
rm DataProtectionCertificate.pfx

- name: Extract identity certificate from env
uses: timheuer/[email protected]
with:
fileDir: './'
fileName: 'DataProtectionCertificate.pfx'
encodedString: ${{ secrets.API_DATA_PROTECTION_CERTIFICATE_FILE_BASE64 }}

# - name: Retrieve AppleAuthKey.p8
# run: echo "${{ secrets.APPSTORE_API_KEY_PRIVATE_KEY_SALES }}" > AppleAuthKey.p8

Expand Down
11 changes: 0 additions & 11 deletions .github/workflows/todo-sample.cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ jobs:
with:
name: server-bundle

- name: Delete DataProtectionCertificate.pfx
run: |
rm DataProtectionCertificate.pfx

- name: Extract identity certificate from env
uses: timheuer/[email protected]
with:
fileDir: './'
fileName: 'DataProtectionCertificate.pfx'
encodedString: ${{ secrets.API_DATA_PROTECTION_CERTIFICATE_FILE_BASE64 }}

- name: Retrieve AppleAuthKey.p8
run: echo "${{ secrets.APPSTORE_API_KEY_PRIVATE_KEY_TODO }}" > AppleAuthKey.p8

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ variables:
APP_SERVICE_NAME: 'app-service-bp-test'
AZURE_SUBSCRIPTION: 'bp-test-service-connection' # https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#azure-resource-manager-service-connection
ConnectionStrings.SqlServerConnectionString: $(DB_CONNECTION_STRING)
DataProtectionCertificatePassword: $(API_DATA_PROTECTION_CERTIFICATE_PASSWORD)
Identity.JwtIssuerSigningKeySecret: $(Jwt_Issuer_Signing_Key_Secret)
ServerAddress: 'https://use-your-api-server-url-here.com/'
WindowsUpdate.FilesUrl: 'https://use-your-api-server-url-here.com/windows' # Deploy the published Windows application files to your desired hosting location and use the host url here.
WebAppRender.BlazorMode: 'BlazorWebAssembly'
Expand Down Expand Up @@ -106,18 +106,6 @@ jobs:
folderPath: './'
targetFiles: 'appsettings.json'

- task: DownloadSecureFile@1
displayName: Download .pfx file
name: DataProtectionCertificate
inputs:
secureFile: 'DataProtectionCertificate.pfx'

- script: |
rm DataProtectionCertificate.pfx
cp "$(DataProtectionCertificate.secureFilePath)" "DataProtectionCertificate.pfx"
failOnStderr: true
displayName: Copy .pfx file

- task: Bash@3
displayName: 'Run migrations'
inputs:
Expand Down
15 changes: 2 additions & 13 deletions src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,8 @@ jobs:
with:
files: 'appsettings.json'
env:
ConnectionStrings_SqlServerConnectionString: ${{ secrets.DB_CONNECTION_STRING }}
DataProtectionCertificatePassword: ${{ secrets.API_DATA_PROTECTION_CERTIFICATE_PASSWORD }}

- name: Delete DataProtectionCertificate.pfx
run: |
rm DataProtectionCertificate.pfx

- name: Extract data protection certificate from env
uses: timheuer/[email protected]
with:
fileDir: './'
fileName: 'DataProtectionCertificate.pfx'
encodedString: ${{ secrets.API_DATA_PROTECTION_CERTIFICATE_FILE_BASE64 }}
ConnectionStrings.SqlServerConnectionString: ${{ secrets.DB_CONNECTION_STRING }}
Identity.JwtIssuerSigningKeySecret: ${{ secrets.Jwt_Issuer_Signing_Key_Secret }}

- name: Run migrations
run: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@
IconName="@BitIconName.RecycleBin" />
}
<BitButton IconOnly AutoLoading
OnClick="ClearData"
Title="Clear data"
OnClick="ClearCache"
Title="Clear cache"
Color="BitColor.SecondaryBackground"
IconName="@BitIconName.Clear" />
</BitStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private string GetMemoryUsage()
return $"{memory / (1024.0 * 1024.0):F2} MB";
}

private async Task ClearData()
private async Task ClearCache()
{
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ protected override async void OnStart()
base.OnStart();

await deviceCoordinator.ApplyTheme(AppInfo.Current.RequestedTheme is AppTheme.Dark);

//-:cnd:noEmit
#if Android
const int minimumSupportedWebViewVersion = 84;
const int minimumSupportedWebViewVersion = 85;
// Download link for Android emulator (x86 or x86_64)
// https://www.apkmirror.com/apk/google-inc/chrome/chrome-84-0-4147-89-release/
// https://www.apkmirror.com/apk/google-inc/android-system-webview/android-system-webview-84-0-4147-111-release/
// https://www.apkmirror.com/apk/google-inc/chrome/chrome-85-0-4183-127-release/
// https://www.apkmirror.com/apk/google-inc/android-system-webview/android-system-webview-85-0-4183-127-release/

if (Version.TryParse(Android.Webkit.WebView.CurrentWebViewPackage?.VersionName, out var webViewVersion) &&
webViewVersion.Major < minimumSupportedWebViewVersion)
Expand All @@ -76,7 +76,7 @@ protected override async void OnStart()
await Launcher.OpenAsync($"https://play.google.com/store/apps/details?id={webViewName}");
}
#endif

//+:cnd:noEmit
await CheckForUpdates();
}
catch (Exception exp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
<PackageVersion Include="FluentEmail.Smtp" Version="3.0.2" />
<PackageVersion Include="FluentStorage" Version="5.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="9.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.OData" Version="9.2.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
<PackageReference Include="FluentStorage" />
<PackageReference Condition=" '$(filesStorage)' == 'AzureBlobStorage' OR '$(filesStorage)' == '' " Include="FluentStorage.Azure.Blobs" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<PackageReference Include="Microsoft.AspNetCore.OData" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
Expand Down Expand Up @@ -60,10 +59,6 @@
<Using Include="Microsoft.AspNetCore.OData.Query" />
<Using Include="Microsoft.AspNetCore.Mvc" />

<None Update="DataProtectionCertificate.pfx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>

<Content Include=".config\dotnet-tools.json" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
//#if (sample == true)
using Boilerplate.Server.Api.Models.Todo;
//#endif
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Boilerplate.Server.Api.Models.Identity;
using Boilerplate.Server.Api.Data.Configurations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
//#if (notification == true)
using Boilerplate.Server.Api.Models.PushNotification;
//#endif
Expand All @@ -20,10 +19,8 @@
namespace Boilerplate.Server.Api.Data;

public partial class AppDbContext(DbContextOptions<AppDbContext> options)
: IdentityDbContext<User, Role, Guid>(options), IDataProtectionKeyContext
: IdentityDbContext<User, Role, Guid>(options)
{
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } = default!;

public DbSet<UserSession> UserSessions { get; set; } = default!;

//#if (sample == true)
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
using Microsoft.AspNetCore.OData;
using Microsoft.Net.Http.Headers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.ResponseCompression;
using System.Security.Cryptography.X509Certificates;
using Twilio;
using System.Text;
using Fido2NetLib;
using PhoneNumbers;
using FluentStorage;
Expand Down Expand Up @@ -331,16 +330,6 @@ private static void AddIdentity(WebApplicationBuilder builder)
configuration.Bind(appSettings);
var identityOptions = appSettings.Identity;

var certificatePath = Path.Combine(AppContext.BaseDirectory, "DataProtectionCertificate.pfx");
var certificate = new X509Certificate2(certificatePath, appSettings.DataProtectionCertificatePassword, AppPlatform.IsWindows ? X509KeyStorageFlags.EphemeralKeySet : X509KeyStorageFlags.DefaultKeySet);

if (env.IsDevelopment() is false && (DateTimeOffset.UtcNow < certificate.NotBefore || DateTimeOffset.UtcNow > certificate.NotAfter))
throw new InvalidOperationException($"The Data Protection certificate is invalid. Current UTC time: {DateTimeOffset.UtcNow}, Certificate valid from: {certificate.NotBefore.ToUniversalTime()}, Certificate valid until: {certificate.NotAfter.ToUniversalTime()}.");

services.AddDataProtection()
.PersistKeysToDbContext<AppDbContext>()
.ProtectKeysWithCertificate(certificate);

services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders()
Expand All @@ -367,7 +356,7 @@ private static void AddIdentity(WebApplicationBuilder builder)
RequireSignedTokens = true,

ValidateIssuerSigningKey = env.IsDevelopment() is false,
IssuerSigningKey = new X509SecurityKey(certificate),
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.Identity.JwtIssuerSigningKeySecret)),

RequireExpirationTime = true,
ValidateLifetime = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:57278/",
"applicationUrl": "http://localhost:55031/",
"httpPort": 5031
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@
//#if (notification == true)
using AdsPush.Abstraction.Settings;
//#endif
using System.Text;
using System.Text.RegularExpressions;
using Boilerplate.Server.Api.Services;

namespace Boilerplate.Server.Api;

public partial class ServerApiSettings : SharedSettings
{
/// <summary>
/// It can also be configured using: dotnet user-secrets set 'DataProtectionCertificatePassword' '@nyPassw0rd'
/// </summary>
[Required]
public string DataProtectionCertificatePassword { get; set; } = default!;

[Required]
public AppIdentityOptions Identity { get; set; } = default!;

Expand Down Expand Up @@ -85,12 +80,20 @@ public override IEnumerable<ValidationResult> Validate(ValidationContext validat
}
Validator.TryValidateObject(ResponseCaching, new ValidationContext(ResponseCaching), validationResults, true);

const int MinimumJwtIssuerSigningKeySecretByteLength = 64; // 512 bits = 64 bytes, minimum for HS512
var jwtIssuerSigningKeySecretByteLength = Encoding.UTF8.GetBytes(Identity.JwtIssuerSigningKeySecret).Length;
if (jwtIssuerSigningKeySecretByteLength <= MinimumJwtIssuerSigningKeySecretByteLength)
{
throw new ArgumentException(
$"The JWT signing key must be greater than {MinimumJwtIssuerSigningKeySecretByteLength} bytes " +
$"({MinimumJwtIssuerSigningKeySecretByteLength * 8} bits) for HS512. Current key is {jwtIssuerSigningKeySecretByteLength} bytes.");
}

if (AppEnvironment.IsDev() is false)
{
if (DataProtectionCertificatePassword is "P@ssw0rdP@ssw0rd")
if (Identity.JwtIssuerSigningKeySecret is "VeryLongJWTIssuerSiginingKeySecretThatIsMoreThan64BytesToEnsureCompatibilityWithHS512Algorithm")
{
throw new InvalidOperationException(@"The default test certificate is still in use. Please replace it with a new one by running the 'dotnet dev-certs https --export-path DataProtectionCertificate.pfx --password @nyPassw0rd'
command in the Server.Api's project's folder and replace P@ssw0rdP@ssw0rd with the new password.");
throw new InvalidOperationException(@"Please replace JwtIssuerSigningKeySecret with a new one.");
}

//#if (captcha == "reCaptcha")
Expand Down Expand Up @@ -132,6 +135,9 @@ internal bool IsAllowedOrigin(Uri origin)

public partial class AppIdentityOptions : IdentityOptions
{
[Required]
public string JwtIssuerSigningKeySecret { get; set; } = default!;

/// <summary>
/// BearerTokenExpiration used as JWT's expiration claim, access token's `expires in` and cookie's `max age`.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public string Protect(AuthenticationTicket data, string? purpose)
Audience = appSettings.Identity.Audience,
IssuedAt = DateTimeOffset.UtcNow.DateTime,
Expires = data.Properties.ExpiresUtc!.Value.UtcDateTime,
SigningCredentials = new SigningCredentials(validationParameters.IssuerSigningKey, SecurityAlgorithms.RsaSha512),
SigningCredentials = new SigningCredentials(validationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha512),
Subject = new ClaimsIdentity(data.Principal.Claims),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@
}
},
//#endif
"DataProtectionCertificatePassword": "P@ssw0rdP@ssw0rd",
"DataProtectionCertificatePassword_Comment": "It can also be configured using: dotnet user-secrets set 'DataProtectionCertificatePassword' '@nyPassw0rd'",
"Identity": {
"JwtIssuerSigningKeySecret": "VeryLongJWTIssuerSiginingKeySecretThatIsMoreThan64BytesToEnsureCompatibilityWithHS512Algorithm",
"Issuer": "Boilerplate",
"Audience": "Boilerplate",
"BearerTokenExpiration": "0.00:05:00",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<PackageReference Condition=" '$(offlineDb)' == 'true' OR '$(offlineDb)' == ''" Include="Microsoft.EntityFrameworkCore.Tools" PrivateAssets="all" />
<PackageReference Condition=" '$(offlineDb)' == 'true' OR '$(offlineDb)' == ''" Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
<PackageReference Condition=" '$(appInsights)' == 'true' OR '$(appInsights)' == '' " Include="Microsoft.ApplicationInsights.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Condition=" '$(sentry)' == 'true' OR '$(sentry)' == '' " Include="Sentry.AspNetCore" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:57278/",
"applicationUrl": "http://localhost:55030/",
"httpPort": 5030
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ public IComponentRenderMode? RenderMode
// you can switch between configurations like `DebugBlazorServer` and `DebugBlazorWasm`.
// If `DebugBlazorServer` is selected, `BlazorMode` will be set to `BlazorServer`
// regardless of its value in appsettings.json
//-:cnd:noEmit
#if DebugBlazorServer
mode = BlazorWebAppMode.BlazorServer;
#elif DebugBlazorWasm
mode = BlazorWebAppMode.BlazorWebAssembly;
#endif
//+:cnd:noEmit

return mode switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Boilerplate.Server.Web.Services;

/// <summary>
/// In standalone API mode, this code only runs during Blazor pre-rendering or Blazor Server.
/// Since the `AppSecureJWTFormat` in the Server.Api project strictly validates access tokens using the provided PFX file,
/// Since the `AppSecureJWTFormat` in the Server.Api project strictly validates access tokens using the provided JwtIssuerSigningKeySecret,
/// strict validation isn't necessary here. Instead, we simply parse the token, similar to how it's handled on the client side (Blazor WASM and Blazor Hybrid).
/// </summary>
public partial class SimpleJwtSecureDataFormat : ISecureDataFormat<AuthenticationTicket>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"AzureBlobStorageSasUrl": "emulator"

},
"DataProtectionCertificatePassword": "P@ssw0rdP@ssw0rd",
"Identity": {
"JwtIssuerSigningKeySecret": "VeryLongJWTIssuerSiginingKeySecretThatIsMoreThan64BytesToEnsureCompatibilityWithHS512Algorithm",
"Issuer": "Boilerplate",
"Audience": "Boilerplate",
"BearerTokenExpiration": "0.00:05:00",
Expand Down Expand Up @@ -93,8 +93,6 @@
"ZoneId": null,
"AdditionalDomains": []
},
//#endif
//#if (signalR == true)
"Azure": {
"SignalR": {
"ConnectionString": null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@
<Using Include="Microsoft.Extensions.Options" />
</ItemGroup>

<ItemGroup>
<None Update="IdentityCertificate.pfx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<Compile Update="**\*.Designer.cs">
<DesignTime>True</DesignTime>
Expand Down
Loading
Loading