-
-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Microsoft.AspNetCore.DataProtection.AzureStorage is deprecated now, use Azure.Extensions.AspNetCore.DataProtection.Blobs instead
- Loading branch information
github-actions
committed
Feb 15, 2021
1 parent
e5c2d39
commit baf95bf
Showing
6 changed files
with
467 additions
and
136 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 30 additions & 30 deletions
60
...rver/Aguacongas.IdentityServer.KeysRotation/Aguacongas.IdentityServer.KeysRotation.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,36 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net5.0</TargetFramework> | ||
<Authors>Olivier Lefebvre</Authors> | ||
<Description>IdentityServer4 signing keys rotation.</Description> | ||
<Copyright>Copyright (c) 2020 @Olivier Lefebvre</Copyright> | ||
<PackageProjectUrl>https://github.com/Aguafrommars/TheIdServer/tree/master/src/IdentityServer/Aguacongas.IdentityServer.KeysRotation</PackageProjectUrl> | ||
<RepositoryUrl>https://github.com/aguacongas/TheIdServer</RepositoryUrl> | ||
<RepositoryType>git</RepositoryType> | ||
<PackageTags>identityserver4;signing keys rotation</PackageTags> | ||
<PackageIconUrl>https://raw.githubusercontent.com/Aguafrommars/TheIdServer/master/package-icon.png</PackageIconUrl> | ||
<PackageLicenseUrl>https://github.com/aguacongas/TheIdServer/blob/master/LICENSE</PackageLicenseUrl> | ||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
<CodeAnalysisRuleSet>..\..\..\.sonarlint\aguacongas_theidservercsharp.ruleset</CodeAnalysisRuleSet> | ||
</PropertyGroup> | ||
<PropertyGroup> | ||
<TargetFramework>net5.0</TargetFramework> | ||
<Authors>Olivier Lefebvre</Authors> | ||
<Description>IdentityServer4 signing keys rotation.</Description> | ||
<Copyright>Copyright (c) 2020 @Olivier Lefebvre</Copyright> | ||
<PackageProjectUrl>https://github.com/Aguafrommars/TheIdServer/tree/master/src/IdentityServer/Aguacongas.IdentityServer.KeysRotation</PackageProjectUrl> | ||
<RepositoryUrl>https://github.com/aguacongas/TheIdServer</RepositoryUrl> | ||
<RepositoryType>git</RepositoryType> | ||
<PackageTags>identityserver4;signing keys rotation</PackageTags> | ||
<PackageIconUrl>https://raw.githubusercontent.com/Aguafrommars/TheIdServer/master/package-icon.png</PackageIconUrl> | ||
<PackageLicenseUrl>https://github.com/aguacongas/TheIdServer/blob/master/LICENSE</PackageLicenseUrl> | ||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
<CodeAnalysisRuleSet>..\..\..\.sonarlint\aguacongas_theidservercsharp.ruleset</CodeAnalysisRuleSet> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<AdditionalFiles Include="..\..\..\.sonarlint\aguacongas_theidserver\CSharp\SonarLint.xml" Link="SonarLint.xml" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<AdditionalFiles Include="..\..\..\.sonarlint\aguacongas_theidserver\CSharp\SonarLint.xml" Link="SonarLint.xml" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="IdentityModel" Version="5.0.1" /> | ||
<PackageReference Include="IdentityServer4" Version="4.1.1" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.AzureStorage" Version="3.1.11" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.5" /> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.8" /> | ||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.8.0" /> | ||
<PackageReference Include="StackExchange.Redis" Version="2.2.4" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.2.0" /> | ||
<PackageReference Include="IdentityModel" Version="5.0.1" /> | ||
<PackageReference Include="IdentityServer4" Version="4.1.1" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.5" /> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.2" /> | ||
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.2.8" /> | ||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.8.0" /> | ||
<PackageReference Include="StackExchange.Redis" Version="2.2.4" /> | ||
</ItemGroup> | ||
|
||
</Project> |
274 changes: 274 additions & 0 deletions
274
...tityServer/Aguacongas.IdentityServer.KeysRotation/AzureKeyVault/AzureBlobXmlRepository.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
// Modifications copyright (c) 2021 @Olivier Lefebvre | ||
|
||
// This code is a copy of https://github.com/Azure/azure-sdk-for-net/blob/d263f23aa3a28ff4fc4366b8dee144d4c0c3ab10/sdk/extensions/Azure.Extensions.AspNetCore.DataProtection.Blobs/src/AzureBlobXmlRepository.cs | ||
// with namespace change from original Azure.Extensions.AspNetCore.DataProtection.Blobs | ||
|
||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.ObjectModel; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Runtime.ExceptionServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using System.Xml; | ||
using System.Xml.Linq; | ||
using Azure; | ||
using Azure.Storage.Blobs; | ||
using Azure.Storage.Blobs.Models; | ||
using Microsoft.AspNetCore.DataProtection.Repositories; | ||
|
||
namespace Aguacongas.IdentityServer.KeysRotation.AzureKeyVault | ||
{ | ||
/// <summary> | ||
/// An <see cref="IXmlRepository"/> which is backed by Azure Blob Storage. | ||
/// </summary> | ||
/// <remarks> | ||
/// Instances of this type are thread-safe. | ||
/// </remarks> | ||
internal sealed class AzureBlobXmlRepository : IXmlRepository | ||
{ | ||
private const int ConflictMaxRetries = 5; | ||
private static readonly TimeSpan ConflictBackoffPeriod = TimeSpan.FromMilliseconds(200); | ||
private static readonly XName RepositoryElementName = "repository"; | ||
private static readonly BlobHttpHeaders _blobHttpHeaders = new() { ContentType = "application/xml; charset=utf-8" }; | ||
|
||
private readonly Random _random; | ||
private BlobData _cachedBlobData; | ||
private readonly BlobClient _blobClient; | ||
|
||
/// <summary> | ||
/// Creates a new instance of the <see cref="AzureBlobXmlRepository"/>. | ||
/// </summary> | ||
/// <param name="blobClient">A <see cref="BlobClient"/> that is connected to the blob we are reading from and writing to.</param> | ||
public AzureBlobXmlRepository(BlobClient blobClient) | ||
{ | ||
_random = new Random(); | ||
_blobClient = blobClient; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public IReadOnlyCollection<XElement> GetAllElements() | ||
{ | ||
// Shunt the work onto a ThreadPool thread so that it's independent of any | ||
// existing sync context or other potentially deadlock-causing items. | ||
|
||
var elements = Task.Run(() => GetAllElementsAsync()).GetAwaiter().GetResult(); | ||
return new ReadOnlyCollection<XElement>(elements); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void StoreElement(XElement element, string friendlyName) | ||
{ | ||
if (element == null) | ||
{ | ||
throw new ArgumentNullException(nameof(element)); | ||
} | ||
|
||
// Shunt the work onto a ThreadPool thread so that it's independent of any | ||
// existing sync context or other potentially deadlock-causing items. | ||
|
||
Task.Run(() => StoreElementAsync(element)).GetAwaiter().GetResult(); | ||
} | ||
|
||
private static XDocument CreateDocumentFromBlob(byte[] blob) | ||
{ | ||
using (var memoryStream = new MemoryStream(blob)) | ||
{ | ||
var xmlReaderSettings = new XmlReaderSettings() | ||
{ | ||
DtdProcessing = DtdProcessing.Prohibit, | ||
IgnoreProcessingInstructions = true | ||
}; | ||
|
||
using (var xmlReader = XmlReader.Create(memoryStream, xmlReaderSettings)) | ||
{ | ||
return XDocument.Load(xmlReader); | ||
} | ||
} | ||
} | ||
|
||
private async Task<IList<XElement>> GetAllElementsAsync() | ||
{ | ||
var data = await GetLatestDataAsync().ConfigureAwait(false); | ||
|
||
if (data == null || data.BlobContents.Length == 0) | ||
{ | ||
// no data in blob storage | ||
return Array.Empty<XElement>(); | ||
} | ||
|
||
// The document will look like this: | ||
// | ||
// <root> | ||
// <child /> | ||
// <child /> | ||
// ... | ||
// </root> | ||
// | ||
// We want to return the first-level child elements to our caller. | ||
|
||
var doc = CreateDocumentFromBlob(data.BlobContents); | ||
return doc.Root.Elements().ToList(); | ||
} | ||
|
||
private async Task<BlobData> GetLatestDataAsync() | ||
{ | ||
// Set the appropriate AccessCondition based on what we believe the latest | ||
// file contents to be, then make the request. | ||
|
||
var latestCachedData = Volatile.Read(ref _cachedBlobData); // local ref so field isn't mutated under our feet | ||
var requestCondition = (latestCachedData != null) | ||
? new BlobRequestConditions() { IfNoneMatch = latestCachedData.ETag } | ||
: null; | ||
|
||
try | ||
{ | ||
using (var memoryStream = new MemoryStream()) | ||
{ | ||
var response = await _blobClient.DownloadToAsync( | ||
destination: memoryStream, | ||
conditions: requestCondition).ConfigureAwait(false); | ||
|
||
if (response.Status == 304) | ||
{ | ||
// 304 Not Modified | ||
// Thrown when we already have the latest cached data. | ||
// This isn't an error; we'll return our cached copy of the data. | ||
return latestCachedData; | ||
} | ||
|
||
// At this point, our original cache either didn't exist or was outdated. | ||
// We'll update it now and return the updated value | ||
latestCachedData = new BlobData() | ||
{ | ||
BlobContents = memoryStream.ToArray(), | ||
ETag = response.Headers.ETag | ||
}; | ||
} | ||
Volatile.Write(ref _cachedBlobData, latestCachedData); | ||
} | ||
catch (RequestFailedException ex) when (ex.Status == 404) | ||
{ | ||
// 404 Not Found | ||
// Thrown when no file exists in storage. | ||
// This isn't an error; we'll delete our cached copy of data. | ||
|
||
latestCachedData = null; | ||
Volatile.Write(ref _cachedBlobData, latestCachedData); | ||
} | ||
|
||
return latestCachedData; | ||
} | ||
|
||
private int GetRandomizedBackoffPeriod() | ||
{ | ||
// returns a TimeSpan in the range [0.8, 1.0) * ConflictBackoffPeriod | ||
// not used for crypto purposes | ||
var multiplier = 0.8 + (_random.NextDouble() * 0.2); | ||
return (int)(multiplier * ConflictBackoffPeriod.Ticks); | ||
} | ||
|
||
[SuppressMessage("Major Bug", "S2259:Null pointers should not be dereferenced", Justification = "Exception is thrown before that.")] | ||
private async Task StoreElementAsync(XElement element) | ||
{ | ||
// holds the last error in case we need to rethrow it | ||
ExceptionDispatchInfo lastError = null; | ||
|
||
for (var i = 0; i < ConflictMaxRetries; i++) | ||
{ | ||
if (i > 1) | ||
{ | ||
// If multiple conflicts occurred, wait a small period of time before retrying | ||
// the operation so that other writers can make forward progress. | ||
await Task.Delay(GetRandomizedBackoffPeriod()).ConfigureAwait(false); | ||
} | ||
|
||
if (i > 0) | ||
{ | ||
// If at least one conflict occurred, make sure we have an up-to-date | ||
// view of the blob contents. | ||
await GetLatestDataAsync().ConfigureAwait(false); | ||
} | ||
|
||
// Merge the new element into the document. If no document exists, | ||
// create a new default document and inject this element into it. | ||
|
||
var latestData = Volatile.Read(ref _cachedBlobData); | ||
var doc = (latestData != null) | ||
? CreateDocumentFromBlob(latestData.BlobContents) | ||
: new XDocument(new XElement(RepositoryElementName)); | ||
doc.Root.Add(element); | ||
|
||
// Turn this document back into a byte[]. | ||
|
||
var serializedDoc = new MemoryStream(); | ||
doc.Save(serializedDoc, SaveOptions.DisableFormatting); | ||
serializedDoc.Position = 0; | ||
|
||
// Generate the appropriate precondition header based on whether or not | ||
// we believe data already exists in storage. | ||
|
||
BlobRequestConditions requestConditions; | ||
if (latestData != null) | ||
{ | ||
requestConditions = new BlobRequestConditions() { IfMatch = latestData.ETag }; | ||
} | ||
else | ||
{ | ||
requestConditions = new BlobRequestConditions() { IfNoneMatch = ETag.All }; | ||
} | ||
|
||
try | ||
{ | ||
// Send the request up to the server. | ||
var response = await _blobClient.UploadAsync( | ||
serializedDoc, | ||
httpHeaders: _blobHttpHeaders, | ||
conditions: requestConditions).ConfigureAwait(false); | ||
|
||
// If we got this far, success! | ||
// We can update the cached view of the remote contents. | ||
|
||
Volatile.Write(ref _cachedBlobData, new BlobData() | ||
{ | ||
BlobContents = serializedDoc.ToArray(), | ||
ETag = response.Value.ETag // was updated by Upload routine | ||
}); | ||
|
||
return; | ||
} | ||
catch (RequestFailedException ex) | ||
when (ex.Status == 409 || ex.Status == 412) | ||
{ | ||
// 409 Conflict | ||
// This error is rare but can be thrown in very special circumstances, | ||
// such as if the blob in the process of being created. We treat it | ||
// as equivalent to 412 for the purposes of retry logic. | ||
|
||
// 412 Precondition Failed | ||
// We'll get this error if another writer updated the repository and we | ||
// have an outdated view of its contents. If this occurs, we'll just | ||
// refresh our view of the remote contents and try again up to the max | ||
// retry limit. | ||
|
||
lastError = ExceptionDispatchInfo.Capture(ex); | ||
} | ||
} | ||
|
||
// if we got this far, something went awry | ||
lastError.Throw(); | ||
} | ||
|
||
private sealed class BlobData | ||
{ | ||
internal byte[] BlobContents; | ||
internal ETag? ETag; | ||
} | ||
} | ||
} |
Oops, something went wrong.