420.6 Keycloak and MVC - chempkovsky/CS82ANGULAR GitHub Wiki
- KeyCloak can be used as authentication and authorisation server
- Once an application is registered with KeyCloak, you can define one or more roles for that application using the KeyCloak console.
- Once you have created a user in KeyCloak, you can assign one or more registered application roles to that user.
- After login into MVC app you can get all the roles assigned to the current user. KeyCloak sends the roles only for the current app. (In case you registered more than one apps and assigned the roles of other apps to the current user).
- you don't have to rely on scopes
- In short, KeyCloak made with love.
- KeyCloak can be used as authentication only server
- In this case, KeyCloak will be used to authenticate the user and Microsoft Identity middleware to authorize the user.
- In this article we show how to configure MVC app to use KeyCloak as authentication only server
- Only login and logout will be discussed
- we just follow the recommended approach
- it is the same
- it is the same
- it is the same
- it is the same
- it is the same
- On the developer machine start Visual Studio.
- Create new project with a template: ASP.NET Core Web App (Model-View-Controler)
- Name: MvcCore01
- Framework: '.Net 8.0' (or '.Net 9.0')
- Authentication type:
Individual accounts - Configure Https: ON
- Do not use top-level statements: ON
- add reference onto Microsoft.AspNetCore.Authentication.OpenIdConnect
- your MvcCore01.csproj become as follows
- Create new project with a template: ASP.NET Core Web App (Model-View-Controler)
Click to show the code
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-MvcCore01-e24e1607-7547-4c8d-b012-ac3a9386bce5</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.17" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.17" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.17" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.17" />
</ItemGroup>
</Project>- modify
launchSettings.jsonfile as follows:
Click to show the code
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://AAA.XXXXXXX.ZZZ:7008",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}- modify
Program.csas follows
Click to show the code
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using MvcCore01.Data;
//
// https://kc.rupbes.by:8445/realms/rupbes.test/account
//
namespace MvcCore01
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>()
;//.AddDefaultTokenProviders();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = "/Account/Login/";
options.LogoutPath = "/Account/Logout";
}
)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "SSO with Keycloak", o =>
{
o.SignInScheme = IdentityConstants.ExternalScheme;
o.SignOutScheme = IdentityConstants.ApplicationScheme;
o.ClientId = "Mvc01";
o.ClientSecret = "XXXXXXXXXXXXXXXXXXXXXXX"; // copy Secret from keycloak console
o.Authority = "https://kc.rupbes.by:8445/realms/rupbes.test";
// default value == "/signin-oidc"
// Keycloak "Valid redirect URIs"-param must be set to "/signin-oidc"
o.CallbackPath = new PathString("/signin-oidc");
// Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme.
//o.RemoteSignOutPath = new PathString("/Home/DoLogout"); // default value
// default value == "/signout-callback-oidc"
// Keycloak "Valid post logout redirect URIs"-param must be set to "/signout-callback-oidc"
// o.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
/// <summary>
/// The uri where the user agent will be redirected to after application is signed out from the identity provider.
/// The redirect will happen after the SignedOutCallbackPath is invoked.
/// </summary>
/// <remarks>This URI can be out of the application's domain. By default it points to the root.</remarks>
// o.SignedOutRedirectUri = new PathString("/Home/DoCleanUpCookie"); // default value
o.ResponseType = OpenIdConnectResponseType.Code;
o.UsePkce = true;
// o.Scope.Add("user.read");
// o.MapInboundClaims = false;
o.Scope.Clear();
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("phone");
// o.Scope.Add("offline_access");
o.SaveTokens = true;
});
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
}
}
}- On the developer machine start Visual Studio.
- Create new project with a template: ASP.NET Core Web App (Model-View-Controler)
- Name: MvcCore01
- Framework: '.Net 8.0' (or '.Net 9.0')
- Authentication type:
Individual accounts - Configure Https: ON
- Do not use top-level statements: ON
- add reference onto Microsoft.AspNetCore.Authentication.OpenIdConnect
- your MvcCore01.csproj become as follows
- Create new project with a template: ASP.NET Core Web App (Model-View-Controler)
Click to show the code
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-MvcCore02-41c22ddb-032a-416a-aaaa-83d1f35fd6ed</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.17" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.17" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.17" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.17" />
</ItemGroup>
</Project>- modify
launchSettings.jsonfile as follows:
Click to show the code
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://BBB.XXXXXXX.ZZZ:7201",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}- modify
Program.csas follows
Click to show the code
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using MvcCore02.Data;
//
// https://kc.rupbes.by:8445/realms/rupbes.test/account
//
namespace MvcCore02
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>()
;//.AddDefaultTokenProviders();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = "/Account/Login/";
options.LogoutPath = "/Account/Logout";
}
)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "SSO with Keycloak", o =>
{
o.SignInScheme = IdentityConstants.ExternalScheme;
o.SignOutScheme = IdentityConstants.ApplicationScheme;
o.ClientId = "Mvc02";
o.ClientSecret = "YYYYYYYYYYYYYYYYYYYYYYY"; // copy Secret from keycloak console
o.Authority = "https://kc.rupbes.by:8445/realms/rupbes.test";
// default value == "/signin-oidc"
// Keycloak "Valid redirect URIs"-param must be set to "/signin-oidc"
o.CallbackPath = new PathString("/signin-oidc");
// Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme.
//o.RemoteSignOutPath = new PathString("/Home/DoLogout"); // default value
// default value == "/signout-callback-oidc"
// Keycloak "Valid post logout redirect URIs"-param must be set to "/signout-callback-oidc"
// o.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
/// <summary>
/// The uri where the user agent will be redirected to after application is signed out from the identity provider.
/// The redirect will happen after the SignedOutCallbackPath is invoked.
/// </summary>
/// <remarks>This URI can be out of the application's domain. By default it points to the root.</remarks>
// o.SignedOutRedirectUri = new PathString("/Home/DoCleanUpCookie"); // default value
o.ResponseType = OpenIdConnectResponseType.Code;
o.UsePkce = true;
// o.Scope.Add("user.read");
// o.MapInboundClaims = false;
o.Scope.Clear();
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("phone");
// o.Scope.Add("offline_access");
o.SaveTokens = true;
});
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
}
}
}- On the
Home-page clickLogin-ref. It will show theloginpage withSSO with Keycloak-button on the right side of the page.- click
SSO with Keycloak-button and login astestuser - The prompt will be shown if to register current user in the Asp.Net app. We agree.
- The new row will be inserted in AspNetUsers. (
testuserwill be inserted without password) - The new row will be inserted in AspNetUserLogins. (Inserted row holds both AspNet UserId and Keycloak UserId)
- The new row will be inserted in AspNetUsers. (
- click
- After logout, the record in keycloak sessions is not deleted (!!!)
- If you log in a second time by clicking the "SSO with Keycloak"-button, the request to enter your username and login will not be completed. (!!!)
- The question is:
- Hown to implement Keycloak logout
- Until the Keycloak session expires, we have no way to log in as another Keycloak user. (by the
SSO with Keycloak-button's click)
- Until the Keycloak session expires, we have no way to log in as another Keycloak user. (by the
- Hown to implement Keycloak logout
- The easiest way is to suggest remembering
https://kc.rupbes.by:8445/realms/rupbes.test/account-ref - Another way is to reimplement Mvc logout page
- Add template Identiry logout item to the project
- modify
OnPost-method in theLogout.cshtml.cs-file as follows:
Click to show the code
public async Task<IActionResult> OnPost(string returnUrl = null)
{
Claim? clm = this.User?.FindFirst("http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod");
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
if ("OpenIdConnect".Equals(clm?.Value, StringComparison.OrdinalIgnoreCase))
{
return new RedirectResult("https://kc.rupbes.by:8445/realms/rupbes.test/account");
}
else
{
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
//return new RedirectResult("https://kc.rupbes.by:8445/realms/rupbes.test/account");
}
else
{
// This needs to be a redirect so that the browser performs a new
// request and the identity for the user gets updated.
return RedirectToPage();
//return new RedirectResult("https://kc.rupbes.by:8445/realms/rupbes.test/account");
}
}
}
}- Reminding the Keycloak user that he is not logged out completely
- When trying to implement logout from OpenIdConnect, the problem
Missing parameters: id_token_hintoccurs.- In our opinion, "Reminder" is better. Exiting
OpenIdConnectclears all sessions not only for the current app
- In our opinion, "Reminder" is better. Exiting