Skip to content
This repository has been archived by the owner on Dec 20, 2018. It is now read-only.

Fixes integrity validation in Identity UI V3 #2054

Merged
merged 1 commit into from
Nov 2, 2018
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
6 changes: 3 additions & 3 deletions src/UI/Areas/Identity/Pages/V3/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,17 @@
asp-fallback-src="~/Identity/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"
asp-fallback-src="~/Identity/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=">
</script>
<script src="~/Identity/js/site.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)
</body>
</html>
</html>
45 changes: 37 additions & 8 deletions test/Identity.Test/IdentityUIScriptsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
Expand All @@ -23,7 +24,7 @@ public class IdentityUIScriptsTest : IDisposable
public IdentityUIScriptsTest(ITestOutputHelper output)
{
_output = output;
_httpClient = new HttpClient();
_httpClient = new HttpClient(new RetryHandler(new HttpClientHandler() { }));
}

public static IEnumerable<object[]> ScriptWithIntegrityData
Expand All @@ -40,15 +41,18 @@ public static IEnumerable<object[]> ScriptWithIntegrityData
[MemberData(nameof(ScriptWithIntegrityData))]
public async Task IdentityUI_ScriptTags_SubresourceIntegrityCheck(ScriptTag scriptTag)
{
string expectedIntegrity;
var sha256Integrity = await GetShaIntegrity(scriptTag, SHA256.Create(), "sha256");
Assert.Equal(scriptTag.Integrity, sha256Integrity);
}

private async Task<string> GetShaIntegrity(ScriptTag scriptTag, HashAlgorithm algorithm, string prefix)
{
using (var respStream = await _httpClient.GetStreamAsync(scriptTag.Src))
using (var alg = SHA256.Create())
{
var hash = alg.ComputeHash(respStream);
expectedIntegrity = "sha256-" + Convert.ToBase64String(hash);
return $"{prefix}-" + Convert.ToBase64String(hash);
}

Assert.Equal(expectedIntegrity, scriptTag.Integrity);
}

public static IEnumerable<object[]> ScriptWithFallbackSrcData
Expand All @@ -66,7 +70,7 @@ public static IEnumerable<object[]> ScriptWithFallbackSrcData
public async Task IdentityUI_ScriptTags_FallbackSourceContent_Matches_CDNContent(ScriptTag scriptTag)
{
var slnDir = GetSolutionDir();
var wwwrootDir = Path.Combine(slnDir, "src", "UI", "wwwroot", "V4");
var wwwrootDir = Path.Combine(slnDir, "src", "UI", "wwwroot", scriptTag.Version);

var cdnContent = await _httpClient.GetStringAsync(scriptTag.Src);
var fallbackSrcContent = File.ReadAllText(
Expand All @@ -77,6 +81,7 @@ public async Task IdentityUI_ScriptTags_FallbackSourceContent_Matches_CDNContent

public struct ScriptTag
{
public string Version;
public string Src;
public string Integrity;
public string FallbackSrc;
Expand All @@ -91,8 +96,9 @@ public override string ToString()
private static List<ScriptTag> GetScriptTags()
{
var slnDir = GetSolutionDir();
var uiDir = Path.Combine(slnDir, "src", "UI", "Areas", "Identity", "Pages", "V4");
var cshtmlFiles = Directory.GetFiles(uiDir, "*.cshtml", SearchOption.AllDirectories);
var uiDirV3 = Path.Combine(slnDir, "src", "UI", "Areas", "Identity", "Pages", "V3");
var uiDirV4 = Path.Combine(slnDir, "src", "UI", "Areas", "Identity", "Pages", "V4");
var cshtmlFiles = GetRazorFiles(uiDirV3).Concat(GetRazorFiles(uiDirV4));

var scriptTags = new List<ScriptTag>();
foreach (var cshtmlFile in cshtmlFiles)
Expand All @@ -104,6 +110,8 @@ private static List<ScriptTag> GetScriptTags()
Assert.NotEmpty(scriptTags);

return scriptTags;

IEnumerable<string> GetRazorFiles(string dir) => Directory.GetFiles(dir, "*.cshtml", SearchOption.AllDirectories);
}

private static List<ScriptTag> GetScriptTags(string cshtmlFile)
Expand All @@ -123,6 +131,7 @@ private static List<ScriptTag> GetScriptTags(string cshtmlFile)

scriptTags.Add(new ScriptTag
{
Version = cshtmlFile.Contains("V3") ? "V3" : "V4",
Src = scriptElement.Source,
Integrity = scriptElement.Integrity,
FallbackSrc = fallbackSrcAttribute?.Value,
Expand Down Expand Up @@ -155,5 +164,25 @@ public void Dispose()
{
_httpClient.Dispose();
}

class RetryHandler : DelegatingHandler
{
public RetryHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage result = null;
for (var i = 0; i < 10; i++)
{
result = await base.SendAsync(request, cancellationToken);
if (result.IsSuccessStatusCode)
{
return result;
}
await Task.Delay(1000);
}
return result;
}
}
}
}