Skip to content

Commit

Permalink
fix: switch to Azure package
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 6 changed files with 467 additions and 136 deletions.
26 changes: 26 additions & 0 deletions THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,30 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

License notice for Azure/azure-sdk-for-net
------------------------------------------

The MIT License (MIT)

Copyright (c) 2015 Microsoft

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



2 changes: 1 addition & 1 deletion src/Aguacongas.TheIdServer/Aguacongas.TheIdServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.2.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Keys" Version="1.0.2" />
<PackageReference Include="Azure.Identity" Version="1.3.0" />
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="5.0.0-*" />
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="5.1.0" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.2" />
Expand Down
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>
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;
}
}
}
Loading

0 comments on commit baf95bf

Please sign in to comment.