Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
afedyanin committed Oct 19, 2023
1 parent 14b237a commit 1e93290
Show file tree
Hide file tree
Showing 36 changed files with 515 additions and 275 deletions.
14 changes: 14 additions & 0 deletions Keycloak4Blazor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9C59A7AC
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp", "src\BlazorApp\BlazorApp.csproj", "{2E141D10-E6F4-45F6-94D1-BFE23D0B1B8F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorBff", "src\BlazorBff\BlazorBff.csproj", "{9A116B39-F9D1-460A-A269-596894B3AA3C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorShared", "src\BlazorShared\BlazorShared.csproj", "{AE623DB2-D790-4901-8BC7-D8F73659B83F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -28,12 +32,22 @@ Global
{2E141D10-E6F4-45F6-94D1-BFE23D0B1B8F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E141D10-E6F4-45F6-94D1-BFE23D0B1B8F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E141D10-E6F4-45F6-94D1-BFE23D0B1B8F}.Release|Any CPU.Build.0 = Release|Any CPU
{9A116B39-F9D1-460A-A269-596894B3AA3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A116B39-F9D1-460A-A269-596894B3AA3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A116B39-F9D1-460A-A269-596894B3AA3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A116B39-F9D1-460A-A269-596894B3AA3C}.Release|Any CPU.Build.0 = Release|Any CPU
{AE623DB2-D790-4901-8BC7-D8F73659B83F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE623DB2-D790-4901-8BC7-D8F73659B83F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE623DB2-D790-4901-8BC7-D8F73659B83F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE623DB2-D790-4901-8BC7-D8F73659B83F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{2E141D10-E6F4-45F6-94D1-BFE23D0B1B8F} = {4B42857C-FD8E-4EB6-B9D6-200FD3E19168}
{9A116B39-F9D1-460A-A269-596894B3AA3C} = {4B42857C-FD8E-4EB6-B9D6-200FD3E19168}
{AE623DB2-D790-4901-8BC7-D8F73659B83F} = {4B42857C-FD8E-4EB6-B9D6-200FD3E19168}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AED32BEA-7168-480C-BA57-E1182383356F}
Expand Down
24 changes: 5 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ Keycloak for Blazor demo
- [C#/NetStandard OpenID Connect Client Library for native Applications](https://github.com/IdentityModel/IdentityModel.OidcClient)
- [Secure ASP.NET Core Blazor WebAssembly](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/?view=aspnetcore-7.0)
- [OpenIDConnect Response Type Confusion](https://stackoverflow.com/questions/29275477/openidconnect-response-type-confusion)


- [Blazor.BFF.OpenIDConnect.Template](https://github.com/damienbod/Blazor.BFF.OpenIDConnect.Template)
- [Securing SPAs and Blazor Applications using the BFF (Backend for Frontend) Pattern - Dominick Baier](https://www.youtube.com/watch?v=hWJuX-8Ur2k)
- [Backend for Frontend (BFF) Security Framework](https://duendesoftware.com/products/bff)

## Keycloak setup

Expand Down Expand Up @@ -44,22 +45,8 @@ http:https://localhost:8080/realms/myrealm/account
- AuthFlow: Standard flow, Direct access grants
- Client Auth - On

Add an audience to a client by using client scopes
- On the left side bar click on “Clients” item.
- Click “blazor-client”
- Open “Client scopes” tab
- Click on “blazor-client-dedicated”, should be on tope of the list of scopes
- From the “Mappers” tab, click “Create a new mapper”
- Pick “Audience” from the list
- specify name: Audience
- include client audience: “blazor-client”
- Click “Save”
Besides “Setup” sub-tab, “Client Scopes” tab has “Evaluate” sub-tab. It might come in handy when you need to figure out effective protocol mappers, effective role scope mappings, the content of access, and id tokens.


Add valid redirect urls: http:https://localhost:5278/*


- Add valid redirect urls: http:https://localhost:5278/*
- Enable CORS on Keycloak +
- Download adapter config

```
Expand All @@ -79,6 +66,5 @@ Add valid redirect urls: http:https://localhost:5278/*
curl --data "grant_type=password&client_id=blazor-client&username=afedyanin&password=afedyanin&client_secret=aNZUREfcTwZjh1qiD095SGQnzL6SQWo0" localhost:8080/realms/myrealm/protocol/openid-connect/token
```

- Enable CORS on Keycloak
-

4 changes: 2 additions & 2 deletions src/BlazorApp/App.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<CascadingAuthenticationState>
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
Expand All @@ -22,4 +22,4 @@
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
</CascadingAuthenticationState>
10 changes: 10 additions & 0 deletions src/BlazorApp/BFF/AntiforgeryHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace BlazorApp.Client.BFF;

public class AntiforgeryHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("X-CSRF", "1");
return base.SendAsync(request, cancellationToken);
}
}
103 changes: 103 additions & 0 deletions src/BlazorApp/BFF/BffAuthenticationStateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;

namespace BlazorApp.Client.BFF;

public class BffAuthenticationStateProvider : AuthenticationStateProvider
{
private static readonly TimeSpan UserCacheRefreshInterval = TimeSpan.FromSeconds(60);

private readonly HttpClient _client;
private readonly ILogger<BffAuthenticationStateProvider> _logger;

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

public BffAuthenticationStateProvider(
HttpClient client,
ILogger<BffAuthenticationStateProvider> logger)
{
_client = client;
_logger = logger;
}

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var user = await GetUser();
var state = new AuthenticationState(user);

// checks periodically for a session state change and fires event
// this causes a round trip to the server
// adjust the period accordingly if that feature is needed
if (user.Identity.IsAuthenticated)
{
_logger.LogInformation("starting background check..");
Timer? timer = null;

timer = new Timer(async _ =>
{
var currentUser = await GetUser(false);
if (currentUser.Identity.IsAuthenticated == false)
{
_logger.LogInformation("user logged out");
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(currentUser)));
await timer.DisposeAsync();
}
}, null, 1000, 5000);
}

return state;
}

private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = true)
{
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;
}

public record ClaimRecord(string Type, object Value);

private async Task<ClaimsPrincipal> FetchUser()
{
try
{
_logger.LogInformation("Fetching user information.");
var response = await _client.GetAsync("bff/user?slide=false");

if (response.StatusCode == HttpStatusCode.OK)
{
var claims = await response.Content.ReadFromJsonAsync<List<ClaimRecord>>();

var identity = new ClaimsIdentity(
nameof(BffAuthenticationStateProvider),
"name",
"role");

foreach (var claim in claims!)
{
identity.AddClaim(new Claim(claim.Type, claim.Value.ToString()));
}

return new ClaimsPrincipal(identity);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Fetching user failed.");
}

return new ClaimsPrincipal(new ClaimsIdentity());
}
}
27 changes: 16 additions & 11 deletions src/BlazorApp/BlazorApp.csproj
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="7.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.12" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.12" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="7.0.12" />

</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BlazorShared\BlazorShared.csproj" />
</ItemGroup>

</Project>
52 changes: 0 additions & 52 deletions src/BlazorApp/Client/Program.cs

This file was deleted.

9 changes: 0 additions & 9 deletions src/BlazorApp/Pages/Authentication.razor

This file was deleted.

16 changes: 4 additions & 12 deletions src/BlazorApp/Pages/FetchData.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
@page "/fetchdata"
@using BlazorWasm.Shared
@inject HttpClient Http

@attribute [Authorize]

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>
Expand Down Expand Up @@ -41,17 +44,6 @@ else

protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}

public class WeatherForecast
{
public DateOnly Date { get; set; }

public int TemperatureC { get; set; }

public string? Summary { get; set; }

public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
}
39 changes: 2 additions & 37 deletions src/BlazorApp/Pages/Index.razor
Original file line number Diff line number Diff line change
@@ -1,40 +1,5 @@
@page "/"

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using System.Security.Claims

@inject AuthenticationStateProvider AuthenticationStateProvider
@page "/"

<PageTitle>Index</PageTitle>

<h1>BlazorWASM Keycloak Authentication</h1>

This application demonstrates how to integrate with Keycloak from BlazorWASM application.

@if (claims.Count() > 0)
{
<h2>User Claims:</h2>
<ul>
@foreach (var claim in claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
}

@code {
private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();

protected override Task OnInitializedAsync() => GetClaimsPrincipalData();

private async Task GetClaimsPrincipalData()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated ?? false)
{
claims = user.Claims;
}
}
}
<CurrentSession />
Loading

0 comments on commit 1e93290

Please sign in to comment.