Skip to content

Commit

Permalink
Init repo
Browse files Browse the repository at this point in the history
  • Loading branch information
damienbod committed Apr 15, 2024
1 parent 42714ae commit de5204c
Show file tree
Hide file tree
Showing 60 changed files with 3,039 additions and 0 deletions.
37 changes: 37 additions & 0 deletions dry.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dry.Server", "dry\Server\dry.Server.csproj", "{483238EB-F9FB-47F1-B598-992DA2437DA1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dry.Shared", "dry\Shared\dry.Shared.csproj", "{B9617BE1-36FF-48F0-AA7C-FA73238245AC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dry.Client", "dry\Client\dry.Client.csproj", "{32E73041-442D-4995-8ED5-447C86C7DB59}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{483238EB-F9FB-47F1-B598-992DA2437DA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{483238EB-F9FB-47F1-B598-992DA2437DA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{483238EB-F9FB-47F1-B598-992DA2437DA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{483238EB-F9FB-47F1-B598-992DA2437DA1}.Release|Any CPU.Build.0 = Release|Any CPU
{B9617BE1-36FF-48F0-AA7C-FA73238245AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9617BE1-36FF-48F0-AA7C-FA73238245AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9617BE1-36FF-48F0-AA7C-FA73238245AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9617BE1-36FF-48F0-AA7C-FA73238245AC}.Release|Any CPU.Build.0 = Release|Any CPU
{32E73041-442D-4995-8ED5-447C86C7DB59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32E73041-442D-4995-8ED5-447C86C7DB59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32E73041-442D-4995-8ED5-447C86C7DB59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32E73041-442D-4995-8ED5-447C86C7DB59}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DBF8473-06FC-43AF-A14F-41F17AB93C7E}
EndGlobalSection
EndGlobal
12 changes: 12 additions & 0 deletions dry/Client/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
39 changes: 39 additions & 0 deletions dry/Client/Pages/DirectApi.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@page "/directapi"
@inject IAntiforgeryHttpClientFactory httpClientFactory
@inject IJSRuntime JSRuntime

<h1>Data from Direct API</h1>

@if (apiData == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Data</th>
</tr>
</thead>
<tbody>
@foreach (var data in apiData)
{
<tr>
<td>@data</td>
</tr>
}
</tbody>
</table>
}

@code {
private string[]? apiData;

protected override async Task OnInitializedAsync()
{
var client = await httpClientFactory.CreateClientAsync();

apiData = await client.GetFromJsonAsync<string[]>("api/DirectApi");
}
}
42 changes: 42 additions & 0 deletions dry/Client/Pages/GraphApiCall.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@page "/graphapicall"
@inject IHttpClientFactory httpClientFactory
@inject IJSRuntime JSRuntime

<h1>Data from Graph API</h1>

@if (apiData == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Data</th>
</tr>
</thead>
<tbody>
@foreach (var data in apiData)
{
<tr>
<td>@data</td>
</tr>
}
</tbody>
</table>
}

@code {
private string[]? apiData;

protected override async Task OnInitializedAsync()
{
var token = await JSRuntime.InvokeAsync<string>("getAntiForgeryToken");

var client = httpClientFactory.CreateClient("authorizedClient");
client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", token);

apiData = await client.GetFromJsonAsync<string[]>("api/GraphApiCalls");
}
}
3 changes: 3 additions & 0 deletions dry/Client/Pages/Index.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@page "/"

<h1>Microsoft Entra ID using cookies</h1>
34 changes: 34 additions & 0 deletions dry/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using dry.Client;
using dry.Client.Services;

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection.Extensions;

using System.Net.Http.Headers;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.TryAddSingleton<AuthenticationStateProvider, HostAuthenticationStateProvider>();
builder.Services.TryAddSingleton(sp => (HostAuthenticationStateProvider)sp.GetRequiredService<AuthenticationStateProvider>());
builder.Services.AddTransient<AuthorizedHandler>();

builder.RootComponents.Add<App>("#app");

builder.Services.AddHttpClient("default", client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});

builder.Services.AddHttpClient("authorizedClient", client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).AddHttpMessageHandler<AuthorizedHandler>();

builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("default"));
builder.Services.AddTransient<IAntiforgeryHttpClientFactory, AntiforgeryHttpClientFactory>();

await builder.Build().RunAsync();
25 changes: 25 additions & 0 deletions dry/Client/Services/AntiforgeryHttpClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.JSInterop;

namespace dry.Client.Services;

public class AntiforgeryHttpClientFactory : IAntiforgeryHttpClientFactory
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IJSRuntime _jSRuntime;

public AntiforgeryHttpClientFactory(IHttpClientFactory httpClientFactory, IJSRuntime jSRuntime)
{
_httpClientFactory = httpClientFactory;
_jSRuntime = jSRuntime;
}

public async Task<HttpClient> CreateClientAsync(string clientName = "authorizedClient")
{
var token = await _jSRuntime.InvokeAsync<string>("getAntiForgeryToken");

var client = _httpClientFactory.CreateClient(clientName);
client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", token);

return client;
}
}
48 changes: 48 additions & 0 deletions dry/Client/Services/AuthorizedHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Net;

namespace dry.Client.Services;

// orig src https://github.com/berhir/BlazorWebAssemblyCookieAuth
public class AuthorizedHandler : DelegatingHandler
{
private readonly HostAuthenticationStateProvider _authenticationStateProvider;

public AuthorizedHandler(HostAuthenticationStateProvider authenticationStateProvider)
{
_authenticationStateProvider = authenticationStateProvider;
}

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
HttpResponseMessage responseMessage;
if (authState.User.Identity!= null && !authState.User.Identity.IsAuthenticated)
{
// if user is not authenticated, immediately set response status to 401 Unauthorized
responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
else
{
responseMessage = await base.SendAsync(request, cancellationToken);
}

if (responseMessage.StatusCode == HttpStatusCode.Unauthorized)
{
var content = await responseMessage.Content.ReadAsStringAsync();

// if server returned 401 Unauthorized, redirect to login page
if (content != null && content.Contains("acr")) // CAE
{
_authenticationStateProvider.CaeStepUp(content);
}
else // standard
{
_authenticationStateProvider.SignIn();
}
}

return responseMessage;
}
}
102 changes: 102 additions & 0 deletions dry/Client/Services/HostAuthenticationStateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using dry.Shared.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http.Json;
using System.Security.Claims;

namespace dry.Client.Services;

// orig src https://github.com/berhir/BlazorWebAssemblyCookieAuth
public class HostAuthenticationStateProvider : AuthenticationStateProvider
{
private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60);

private const string LogInPath = "api/Account/Login";
private const string LogOutPath = "api/Account/Logout";

private readonly NavigationManager _navigation;
private readonly HttpClient _client;
private readonly ILogger<HostAuthenticationStateProvider> _logger;

private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0);
private ClaimsPrincipal _cachedUser = new(new ClaimsIdentity());

public HostAuthenticationStateProvider(NavigationManager navigation, HttpClient client, ILogger<HostAuthenticationStateProvider> logger)
{
_navigation = navigation;
_client = client;
_logger = logger;
}

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
return new AuthenticationState(await GetUser(useCache: true));
}

public void SignIn(string? customReturnUrl = null)
{
var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri);
var logInUrl = _navigation.ToAbsoluteUri($"{LogInPath}?returnUrl={encodedReturnUrl}");
_navigation.NavigateTo(logInUrl.ToString(), true);
}

public void CaeStepUp(string claimsChallenge, string? customReturnUrl = null)
{
var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri);
var logInUrl = _navigation.ToAbsoluteUri($"{LogInPath}?claimsChallenge={claimsChallenge}&returnUrl={encodedReturnUrl}");
_navigation.NavigateTo(logInUrl.ToString(), true);
}

private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
{
var now = DateTimeOffset.Now;
if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
{
_logger.LogDebug("Taking user from cache");
return _cachedUser;
}

_logger.LogDebug("Fetching user");
_cachedUser = await FetchUser();
_userLastCheck = now;

return _cachedUser;
}

private async Task<ClaimsPrincipal> FetchUser()
{
UserInfo? user = null;

try
{
_logger.LogInformation("{clientBaseAddress}", _client.BaseAddress?.ToString());
user = await _client.GetFromJsonAsync<UserInfo>("api/User");
}
catch (Exception exc)
{
_logger.LogWarning(exc, "Fetching user failed.");
}

if (user == null || !user.IsAuthenticated)
{
return new ClaimsPrincipal(new ClaimsIdentity());
}

var identity = new ClaimsIdentity(
nameof(HostAuthenticationStateProvider),
user.NameClaimType,
user.RoleClaimType);

if (user.Claims != null)
{
foreach (var claim in user.Claims)
{
identity.AddClaim(new Claim(claim.Type, claim.Value));
}
}

return new ClaimsPrincipal(identity);
}
}
6 changes: 6 additions & 0 deletions dry/Client/Services/IAntiforgeryHttpClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace dry.Client.Services;

public interface IAntiforgeryHttpClientFactory
{
Task<HttpClient> CreateClientAsync(string clientName = "authorizedClient");
}
20 changes: 20 additions & 0 deletions dry/Client/Shared/AntiForgeryTokenInput.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@inject IJSRuntime JSRuntime

<input type="hidden" id="__RequestVerificationToken"
name="__RequestVerificationToken" value="@GetToken()">

@code {

private string token = "";

protected override async Task OnInitializedAsync()
{
token = await JSRuntime.InvokeAsync<string>("getAntiForgeryToken");
}

public string GetToken()
{
return token;
}

}
Loading

0 comments on commit de5204c

Please sign in to comment.