Skip to content

Commit a8bb111

Browse files
fboucherCopilot
andcommitted
feat: Add Keycloak authentication and authorization
- Integrated Keycloak via Aspire.Hosting.Keycloak package - Added OpenID Connect authentication to BlazorApp with Keycloak provider - Configured home page as public, all other pages require authentication - Added Login/Logout UI components in top-right corner - Configured id_token_hint for proper logout flow - Added comprehensive Keycloak setup documentation - Updated .gitignore to exclude Development settings files This implements private website access control where only selected users can authenticate through Keycloak. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1ec90bf commit a8bb111

10 files changed

Lines changed: 78 additions & 25 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ NoteBookmark.Api/obj/
490490
NoteBookmark.Api/appsettings.Development.json
491491

492492
NoteBookmark.BlazorApp/appsettings.Development.json
493+
src/NoteBookmark.BlazorApp/appsettings.Development.json
493494
.azure
494495

495496
NoteBookmark.AppHost/appsettings.Development.json

Directory.Packages.props

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
<Project>
22
<ItemGroup>
33
<!-- Aspire packages -->
4-
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.0.2" />
5-
<PackageVersion Include="Aspire.Hosting.Azure.Storage" Version="13.0.2" />
6-
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.0.2-preview.1.25603.5" />
7-
<PackageVersion Include="Aspire.Hosting.Keycloak" Version="13.1.0-preview.1.25616.3" />
8-
<PackageVersion Include="Aspire.Azure.Data.Tables" Version="13.0.2" />
9-
<PackageVersion Include="Aspire.Azure.Storage.Blobs" Version="13.0.2" />
4+
<PackageVersion Include="Aspire.Hosting.Azure.Storage" Version="13.1.1" />
5+
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.1-preview.1.26105.8" />
6+
<PackageVersion Include="Aspire.Hosting.Keycloak" Version="13.1.1-preview.1.26105.8" />
7+
<PackageVersion Include="Aspire.Azure.Data.Tables" Version="13.1.1" />
8+
<PackageVersion Include="Aspire.Azure.Storage.Blobs" Version="13.1.1" />
109
<!-- Azure packages -->
1110
<PackageVersion Include="Azure.Data.Tables" Version="12.11.0" />
1211
<PackageVersion Include="Azure.Storage.Blobs" Version="12.26.0" />

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ Next time you will navigate to the app, you will be prompt a to login with your
4949

5050
Voila! Your app is now secure.
5151

52+
## Documentation
53+
54+
For detailed setup guides and configuration information:
55+
- [Keycloak Authentication Setup](/docs/KEYCLOAK_AUTH.md) - Complete guide for setting up Keycloak authentication
56+
5257
## Contributing
5358

5459
Your contributions are welcome! Take a look at [CONTRIBUTING](/CONTRIBUTING.md) for details.

src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
2-
<Sdk Name="Aspire.AppHost.Sdk" Version="13.0.2" />
1+
<Project Sdk="Aspire.AppHost.Sdk/13.1.1">
32
<PropertyGroup>
43
<OutputType>Exe</OutputType>
54
<IsAspireHost>true</IsAspireHost>
65
<UserSecretsId>0784f0a9-b1e6-4e65-8d31-00f1369f6d75</UserSecretsId>
76
</PropertyGroup>
87
<ItemGroup>
9-
<PackageReference Include="Aspire.Hosting.AppHost" />
108
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
119
<PackageReference Include="Aspire.Hosting.Docker" />
1210
<PackageReference Include="Aspire.Hosting.Keycloak" />

src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
<FluentHeader>
99
Note Bookmark
1010
<FluentSpacer />
11-
<LoginDisplay />
12-
<FluentAnchor Href="/settings" aria-label="Settings">
13-
<FluentIcon Value="@(new Icons.Filled.Size16.AppsSettings())" />
14-
</FluentAnchor>
11+
<FluentStack Orientation="Orientation.Horizontal" HorizontalGap="12" VerticalAlignment="VerticalAlignment.Center" HorizontalAlignment="HorizontalAlignment.Right">
12+
<LoginDisplay />
13+
<FluentAnchor Href="/settings" aria-label="Settings">
14+
<FluentIcon Value="@(new Icons.Filled.Size16.AppsSettings())" />
15+
</FluentAnchor>
16+
</FluentStack>
1517
</FluentHeader>
1618
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
1719
<NavMenu />

src/NoteBookmark.BlazorApp/Components/Pages/Home.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@page "/"
2+
@attribute [AllowAnonymous]
3+
@using Microsoft.AspNetCore.Authorization
24
@using Microsoft.FluentUI.AspNetCore.Components
35
@inject NavigationManager Navigation
46

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
@page "/login"
2+
@attribute [AllowAnonymous]
3+
@using Microsoft.AspNetCore.Authorization
24
@using Microsoft.AspNetCore.Authentication
35
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
46
@inject NavigationManager Navigation
7+
@inject IHttpContextAccessor HttpContextAccessor
58
@code {
69
protected override async Task OnInitializedAsync()
710
{
8-
// Redirect to Keycloak login
9-
Navigation.NavigateTo($"/authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}", forceLoad: true);
11+
// Get the return URL from query string or default to home
12+
var uri = new Uri(Navigation.Uri);
13+
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
14+
var returnUrl = query["returnUrl"] ?? "/";
15+
16+
// Trigger authentication challenge via HttpContext
17+
var httpContext = HttpContextAccessor.HttpContext;
18+
if (httpContext != null)
19+
{
20+
var authProperties = new AuthenticationProperties
21+
{
22+
RedirectUri = returnUrl
23+
};
24+
await httpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
25+
}
1026
}
1127
}
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
@page "/logout"
2+
@attribute [AllowAnonymous]
3+
@using Microsoft.AspNetCore.Authorization
24
@using Microsoft.AspNetCore.Authentication
35
@using Microsoft.AspNetCore.Authentication.Cookies
46
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
5-
@inject NavigationManager Navigation
7+
@inject IHttpContextAccessor HttpContextAccessor
68
@code {
79
protected override async Task OnInitializedAsync()
810
{
9-
// Redirect to logout endpoint
10-
Navigation.NavigateTo("/authentication/logout", forceLoad: true);
11+
var httpContext = HttpContextAccessor.HttpContext;
12+
if (httpContext != null)
13+
{
14+
var properties = new AuthenticationProperties
15+
{
16+
RedirectUri = "/"
17+
};
18+
await httpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, properties);
19+
await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
20+
}
1121
}
1222
}
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1+
@rendermode InteractiveServer
12
@using Microsoft.AspNetCore.Components.Authorization
23
@inject NavigationManager Navigation
34

45
<AuthorizeView>
56
<Authorized>
6-
<FluentStack Orientation="Orientation.Horizontal" HorizontalGap="8">
7+
<FluentStack Orientation="Orientation.Horizontal" HorizontalGap="8" HorizontalAlignment="HorizontalAlignment.Right" VerticalAlignment="VerticalAlignment.Center">
78
<span>Hello, @context.User.Identity?.Name</span>
8-
<FluentButton Appearance="Appearance.Lightweight" OnClick="Logout">
9-
<FluentIcon Value="@(new Icons.Regular.Size16.ArrowExit())" />
9+
<FluentButton Appearance="Appearance.Lightweight" OnClick="Logout" IconStart="@(new Icons.Regular.Size16.ArrowExit())">
1010
Logout
1111
</FluentButton>
1212
</FluentStack>
1313
</Authorized>
1414
<NotAuthorized>
15-
<FluentButton Appearance="Appearance.Accent" OnClick="Login">
16-
<FluentIcon Value="@(new Icons.Regular.Size16.Person())" />
15+
<FluentButton Appearance="Appearance.Accent" OnClick="Login" IconStart="@(new Icons.Regular.Size16.Person())">
1716
Login
1817
</FluentButton>
1918
</NotAuthorized>
@@ -22,11 +21,16 @@
2221
@code {
2322
private void Login()
2423
{
25-
Navigation.NavigateTo("/login", forceLoad: true);
24+
var returnUrl = Navigation.ToBaseRelativePath(Navigation.Uri);
25+
if (string.IsNullOrEmpty(returnUrl))
26+
{
27+
returnUrl = "/";
28+
}
29+
Navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(returnUrl)}", forceLoad: false);
2630
}
2731

2832
private void Logout()
2933
{
30-
Navigation.NavigateTo("/logout", forceLoad: true);
34+
Navigation.NavigateTo("/logout", forceLoad: false);
3135
}
3236
}

src/NoteBookmark.BlazorApp/Program.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,26 @@
8787
NameClaimType = "preferred_username",
8888
RoleClaimType = "roles"
8989
};
90+
91+
// Configure logout to properly pass id_token_hint to Keycloak
92+
options.Events = new OpenIdConnectEvents
93+
{
94+
OnRedirectToIdentityProviderForSignOut = context =>
95+
{
96+
// Get the id_token from saved tokens
97+
var idToken = context.HttpContext.GetTokenAsync("id_token").Result;
98+
if (!string.IsNullOrEmpty(idToken))
99+
{
100+
context.ProtocolMessage.IdTokenHint = idToken;
101+
}
102+
return Task.CompletedTask;
103+
}
104+
};
90105
});
91106

92107
builder.Services.AddAuthorization();
93108
builder.Services.AddCascadingAuthenticationState();
109+
builder.Services.AddHttpContextAccessor();
94110

95111
// Add services to the container.
96112
builder.Services.AddRazorComponents()

0 commit comments

Comments
 (0)