Skip to content

Commit 0358a47

Browse files
committed
Add implementation for loading signing keys from Key Vault skoruba#533
1 parent 64047b0 commit 0358a47

File tree

10 files changed

+210
-1
lines changed

10 files changed

+210
-1
lines changed

Skoruba.IdentityServer4.Admin.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Skoruba.IdentityServer4.Adm
5151
EndProject
5252
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Skoruba.IdentityServer4.Shared", "src\Skoruba.IdentityServer4.Shared\Skoruba.IdentityServer4.Shared.csproj", "{61B285F0-EE06-4AEE-AAF3-71492CBD11C5}"
5353
EndProject
54+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Skoruba.IdentityServer4.STS.KeyVault", "src\Skoruba.IdentityServer4.STS.KeyVault\Skoruba.IdentityServer4.STS.KeyVault.csproj", "{21AAAF8E-1A81-4A9E-8A69-E5375ECB81E6}"
55+
EndProject
5456
Global
5557
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5658
Debug|Any CPU = Debug|Any CPU
@@ -133,6 +135,10 @@ Global
133135
{61B285F0-EE06-4AEE-AAF3-71492CBD11C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
134136
{61B285F0-EE06-4AEE-AAF3-71492CBD11C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
135137
{61B285F0-EE06-4AEE-AAF3-71492CBD11C5}.Release|Any CPU.Build.0 = Release|Any CPU
138+
{21AAAF8E-1A81-4A9E-8A69-E5375ECB81E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
139+
{21AAAF8E-1A81-4A9E-8A69-E5375ECB81E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
140+
{21AAAF8E-1A81-4A9E-8A69-E5375ECB81E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
141+
{21AAAF8E-1A81-4A9E-8A69-E5375ECB81E6}.Release|Any CPU.Build.0 = Release|Any CPU
136142
EndGlobalSection
137143
GlobalSection(SolutionProperties) = preSolution
138144
HideSolutionNode = FALSE
@@ -156,6 +162,7 @@ Global
156162
{0A8A0DB7-0509-4DFB-9201-74398511B481} = {2A514C8F-6A53-41CA-AB41-B644E7BC92A7}
157163
{4D123ACB-ACBD-4E40-AE6B-1B0F79D703B0} = {0BC0CC4E-A0F1-45E8-B41A-AE0FA76BF3E5}
158164
{61B285F0-EE06-4AEE-AAF3-71492CBD11C5} = {EE588CE5-51D0-4E98-A2B3-40EC8E655931}
165+
{21AAAF8E-1A81-4A9E-8A69-E5375ECB81E6} = {63D44665-AC4C-45F4-A2C7-A7DB394F44C4}
159166
EndGlobalSection
160167
GlobalSection(ExtensibilityGlobals) = postSolution
161168
SolutionGuid = {B3166EDE-037B-4C68-BEBA-5DE9C5E3DB82}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Skoruba.IdentityServer4.STS.Identity.Configuration
2+
{
3+
public class TokenSigningConfiguration
4+
{
5+
public static bool UseAzureKeyVault = true;
6+
}
7+
}

src/Skoruba.IdentityServer4.STS.Identity/Skoruba.IdentityServer4.STS.Identity.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<ProjectReference Include="..\Skoruba.IdentityServer4.Admin.EntityFramework.SqlServer\Skoruba.IdentityServer4.Admin.EntityFramework.SqlServer.csproj" />
5454
<ProjectReference Include="..\Skoruba.IdentityServer4.Admin.EntityFramework\Skoruba.IdentityServer4.Admin.EntityFramework.csproj" />
5555
<ProjectReference Include="..\Skoruba.IdentityServer4.Shared\Skoruba.IdentityServer4.Shared.csproj" />
56+
<ProjectReference Include="..\Skoruba.IdentityServer4.STS.KeyVault\Skoruba.IdentityServer4.STS.KeyVault.csproj" />
5657
</ItemGroup>
5758

5859
<ItemGroup>

src/Skoruba.IdentityServer4.STS.Identity/Startup.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Skoruba.IdentityServer4.STS.Identity.Helpers;
1414
using System;
1515
using Microsoft.AspNetCore.DataProtection;
16+
using Skoruba.IdentityServer4.STS.KeyVault;
1617

1718
namespace Skoruba.IdentityServer4.STS.Identity
1819
{
@@ -39,6 +40,8 @@ public void ConfigureServices(IServiceCollection services)
3940
.SetApplicationName("Skoruba.IdentityServer4")
4041
.PersistKeysToDbContext<IdentityServerDataProtectionDbContext>();
4142

43+
RegisterKeyVaultSingingKeyFeature(services, Configuration);
44+
4245
// Add email senders which is currently setup for SendGrid and SMTP
4346
services.AddEmailSenders(Configuration);
4447

@@ -125,6 +128,16 @@ public virtual void RegisterHstsOptions(IServiceCollection services)
125128
});
126129
}
127130

131+
public virtual void RegisterKeyVaultSingingKeyFeature(IServiceCollection services, IConfiguration configuration)
132+
{
133+
if (TokenSigningConfiguration.UseAzureKeyVault)
134+
{
135+
return;
136+
}
137+
138+
services.AddKeyVaultSigningKeyFeature(configuration);
139+
}
140+
128141
protected IRootConfiguration CreateRootConfiguration()
129142
{
130143
var rootConfiguration = new RootConfiguration();

src/Skoruba.IdentityServer4.STS.Identity/appsettings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,9 @@
6666
"AdvancedConfiguration": {
6767
"PublicOrigin": ""
6868
},
69-
"BasePath": ""
69+
"BasePath": "",
70+
"AzureKeyVaultConfiguration": {
71+
"KeyVaultUri": "",
72+
"KeyName": ""
73+
}
7074
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Skoruba.IdentityServer4.STS.KeyVault.Configuration
2+
{
3+
public class AzureKeyVaultConfiguration
4+
{
5+
public string KeyVaultUri { get; set; }
6+
public string KeyName { get; set; }
7+
public string KeyIdentifier => $"{KeyVaultUri}/keys/{KeyName}";
8+
}
9+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using IdentityServer4.Services;
2+
using IdentityServer4.Stores;
3+
using Microsoft.Azure.KeyVault;
4+
using Microsoft.Azure.Services.AppAuthentication;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Skoruba.IdentityServer4.STS.KeyVault.Configuration;
8+
using Skoruba.IdentityServer4.STS.KeyVault.Stores;
9+
using Skoruba.IdentityServer4.STS.KeyVault.Tokens;
10+
11+
namespace Skoruba.IdentityServer4.STS.KeyVault
12+
{
13+
public static class ServiceCollectionExtensions
14+
{
15+
public static IServiceCollection AddKeyVaultSigningKeyFeature(
16+
this IServiceCollection services,
17+
IConfiguration configuration)
18+
{
19+
var azureServiceTokenProvider = new AzureServiceTokenProvider();
20+
var authenticationCallback = new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback);
21+
var keyVaultClient = new KeyVaultClient(authenticationCallback);
22+
var azureKeyVaultConfiguration = new AzureKeyVaultConfiguration();
23+
configuration.GetSection("AzureKeyVaultConfiguration").Bind(azureKeyVaultConfiguration);
24+
25+
services.AddSingleton<IKeyVaultClient>(keyVaultClient)
26+
.AddSingleton(azureKeyVaultConfiguration)
27+
.AddTransient<ITokenCreationService, KeyVaultTokenCreationService>()
28+
.AddTransient<ISigningCredentialStore, AzureKeyVaultKeyStore>()
29+
.AddTransient<IValidationKeysStore, AzureKeyVaultKeyStore>();
30+
31+
return services;
32+
}
33+
}
34+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.0.3" />
9+
<PackageReference Include="IdentityServer4" Version="3.1.2" />
10+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.3" />
11+
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="3.1.3" />
12+
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.5" />
13+
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.4.0" />
14+
</ItemGroup>
15+
</Project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using IdentityServer4.Models;
6+
using IdentityServer4.Stores;
7+
using Microsoft.Azure.KeyVault;
8+
using Microsoft.IdentityModel.Tokens;
9+
using Skoruba.IdentityServer4.STS.KeyVault.Configuration;
10+
11+
namespace Skoruba.IdentityServer4.STS.KeyVault.Stores
12+
{
13+
public class AzureKeyVaultKeyStore : ISigningCredentialStore, IValidationKeysStore
14+
{
15+
private const string SigningAlgorithm = "PS256";
16+
17+
private readonly AzureKeyVaultConfiguration _configuration;
18+
private readonly IKeyVaultClient _keyVaultClient;
19+
20+
public AzureKeyVaultKeyStore(
21+
AzureKeyVaultConfiguration configuration,
22+
IKeyVaultClient keyVaultClient)
23+
{
24+
_configuration = configuration;
25+
_keyVaultClient = keyVaultClient;
26+
}
27+
28+
public async Task<SigningCredentials> GetSigningCredentialsAsync()
29+
{
30+
var response = await _keyVaultClient.GetKeyAsync(_configuration.KeyIdentifier);
31+
var key = new RsaSecurityKey(response.Key.ToRSA())
32+
{
33+
KeyId = response.KeyIdentifier.Version
34+
};
35+
36+
return new SigningCredentials(key, SigningAlgorithm);
37+
}
38+
39+
public async Task<IEnumerable<SecurityKeyInfo>> GetValidationKeysAsync()
40+
{
41+
var validationKeys = new List<SecurityKeyInfo>();
42+
var keyItemPage = await _keyVaultClient.GetKeyVersionsAsync(_configuration.KeyVaultUri, _configuration.KeyName);
43+
44+
while (true)
45+
{
46+
var validKeys = keyItemPage.Where(key =>
47+
key.Attributes?.Enabled == true &&
48+
key.Attributes?.Expires > DateTime.UtcNow);
49+
50+
foreach (var keyItem in validKeys)
51+
{
52+
var keyBundle = await _keyVaultClient.GetKeyAsync(keyItem.Identifier.Identifier);
53+
var key = new RsaSecurityKey(keyBundle.Key.ToRSA())
54+
{
55+
KeyId = keyBundle.KeyIdentifier.Version
56+
};
57+
58+
validationKeys.Add(new SecurityKeyInfo
59+
{
60+
Key = key,
61+
SigningAlgorithm = SigningAlgorithm
62+
});
63+
}
64+
65+
if (keyItemPage.NextPageLink == null)
66+
{
67+
break;
68+
}
69+
70+
keyItemPage = await _keyVaultClient.GetKeyVersionsNextAsync(keyItemPage.NextPageLink);
71+
}
72+
73+
return validationKeys;
74+
}
75+
}
76+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.IdentityModel.Tokens.Jwt;
2+
using System.Text;
3+
using System.Threading.Tasks;
4+
using IdentityServer4.Configuration;
5+
using IdentityServer4.Services;
6+
using Microsoft.AspNetCore.Authentication;
7+
using Microsoft.Azure.KeyVault;
8+
using Microsoft.Extensions.Logging;
9+
using Skoruba.IdentityServer4.STS.KeyVault.Configuration;
10+
11+
namespace Skoruba.IdentityServer4.STS.KeyVault.Tokens
12+
{
13+
public class KeyVaultTokenCreationService : DefaultTokenCreationService
14+
{
15+
private readonly AzureKeyVaultConfiguration _configuration;
16+
private readonly IKeyVaultClient _keyVaultClient;
17+
18+
public KeyVaultTokenCreationService(
19+
AzureKeyVaultConfiguration configuration,
20+
ISystemClock clock,
21+
IKeyMaterialService keys,
22+
IdentityServerOptions options,
23+
ILogger<DefaultTokenCreationService> logger,
24+
IKeyVaultClient keyVaultClient)
25+
: base(clock, keys, options, logger)
26+
{
27+
_configuration = configuration;
28+
_keyVaultClient = keyVaultClient;
29+
}
30+
31+
protected override async Task<string> CreateJwtAsync(JwtSecurityToken jwt)
32+
{
33+
var plaintext = $"{jwt.EncodedHeader}.{jwt.EncodedPayload}";
34+
35+
using var hasher = CryptoHelper.GetHashAlgorithmForSigningAlgorithm(jwt.SignatureAlgorithm);
36+
var hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(plaintext));
37+
38+
var response = await _keyVaultClient.SignAsync(_configuration.KeyIdentifier, jwt.SignatureAlgorithm, hash);
39+
40+
return $"{plaintext}.{Base64UrlTextEncoder.Encode(response.Result)}";
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)