420.5 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 and authorisation server
- Only login and logout will be discussed
- We just follow the recommended approach
- in the Keycloak console create realm with
rupbes.test-name and default settings
- in the Keycloak console under
rupbes.testrealm we create app- Client ID:
Mvc01 -
Access settings
- Root URL:
https://aaa.xxxxxxx.zzz:7008 - Home URL:
https://aaa.xxxxxxx.zzz:7008 - Valid redirect URIs:
signin-oidc - Valid post logout redirect URIs:
/signout-callback-oidc
- Root URL:
-
Capability config
- Client authentication: ON
- Authentication flow: [
Standard flow]
- Credetials
- Client Authenticator:
Client ID and Secret - Client Secret:
XXXXXXXXXXXXXX(we just copied auto generated string in the clipboard)
- Client Authenticator:
- Client ID:
- in the Keycloak console under
rupbes.testrealm we create app- Client ID:
Mvc02 -
Access settings
- Root URL:
https://bbb.xxxxxxx.zzz:7201 - Home URL:
https://bbb.xxxxxxx.zzz:7201 - Valid redirect URIs:
signin-oidc - Valid post logout redirect URIs:
/signout-callback-oidc
- Root URL:
-
Capability config
- Client authentication: ON
- Authentication flow: [
Standard flow]
- Credetials
- Client Authenticator:
Client ID and Secret - Client Secret:
YYYYYYYYYYYYYYY(we just copied auto generated string in the clipboard)
- Client Authenticator:
- Client ID:
- in the Keycloak console under
rupbes.testrealm we create user- User name:
testuser - Email verified: ON
- Credetials tab
- we set the password for the user
- User name:
- On the developer machine add two lines into
hosts-file.
127.0.0.1 AAA.XXXXXXX.ZZZ
127.0.0.1 BBB.XXXXXXX.ZZZ
- If to use 'localhost' for both app with defferent ports
- you start both app
- you login into Mvc01 app
- After logging into the Mvc02 app you will see that
- you automatically logged out from Mvc01 app
- to avoid such a scenario we will use different dns names
- 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: None
- Configure Https: ON
- Do not use top-level statements: ON
- Create new project with a template:
- add reference onto
Microsoft.AspNetCore.Authentication.OpenIdConnect- your
MvcCore01.csprojbecome as follows
- your
Click to show the code
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" 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.IdentityModel.Protocols.OpenIdConnect;
namespace MvcCore01
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "SSO", o =>
{
//o.SignInScheme = IdentityConstants.ExternalScheme;
//o.SignOutScheme = OpenIdConnectDefaults.AuthenticationScheme; // IdentityConstants.ApplicationScheme;
o.ClientId = "Mvc01";
o.ClientSecret = "YYYYYYYYYYYYYYYYY"; // copy ClientSecret from the Keycloak
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");
// 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.Clear();
o.Scope.Add("openid");
o.Scope.Add("profile");
o.Scope.Add("email");
o.Scope.Add("phone");
o.SaveTokens = true;
});
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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.UseAuthentication(); // Added
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
}
}- in the
HomeController.cs-class add three methods:- both implementation of the
DoLogout-method works:
- both implementation of the
Click to show the code
...
public IActionResult DoLogin()
{
// defining "RedirectUri" is important !!!
var authProps = new AuthenticationProperties
{
RedirectUri = "/"
};
// do not use HttpContext.ChallengeAsync(...)-method call !!! It will not work
return Challenge(authProps, OpenIdConnectDefaults.AuthenticationScheme);
}
// public async Task DoLogout()
// {
// await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
// }
public IActionResult DoLogout()
{
return new SignOutResult(
new[] {
OpenIdConnectDefaults.AuthenticationScheme
// CookieAuthenticationDefaults.AuthenticationScheme // do not insert this line
});
}
public async Task DoCleanUpCookie()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
...- add a second project to the solution and repeat almost the same as for MvcCore01
- name the project
MvcCore02
- name the 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"
}
}
}
}- in
Program.cs-file we changeClientIdandClientSecret
...
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "SSO", o =>
{
o.ClientId = "Mvc02";
o.ClientSecret = "YYYYYYYYYYYYYYYYYYYYYYYYYY"; // copy Secret from keycloak
...- start
MvcCore01app and login astestuser- you must goto
https://aaa.xxxxxxx.zzz:7008/home/DoLogin
- you must goto
- using Keycloak console goto
Sessionsforrupbes.test-realm
testuser REGULAR 7/18/2025, 1:40:18 PM 7/18/2025, 1:40:18 PM 10.183.96.1 Mvc01
- start
MvcCore02app and log in astestuser- you must goto
https://bbb.xxxxxxx.zzz:7201/home/DoLogin
- you must goto
- using Keycloak console goto
Sessionsforrupbes.test-realm- now you see two apps shown
Mvc01 Mvc02
- now you see two apps shown
testuser REGULAR 7/18/2025, 1:40:18 PM 7/18/2025, 1:40:18 PM 10.183.96.1 Mvc01 Mvc02
- log out from only one app, for instance from the second app (or from the first one)
- you must goto
https://bbb.xxxxxxx.zzz:7201/home/DoLogout(orhttps://aaa.xxxxxxx.zzz:7008/home/DoLogoutn)
- you must goto
- using Keycloak console goto
Sessionsforrupbes.test-realm-
The list of Sessions is empty
- We expected that only one session would be deleted. (!!!)
-
The list of Sessions is empty
- for the
MvcCore01app in the Program.cs addoffline_accessscope
o.Scope.Add("offline_access");
- run the both apps
- log in into
MvcCore01- goto
https://aaa.xxxxxxx.zzz:7008/home/DoLogin - check the sessions. We have
OFFLINE-type
- goto
testuser OFFLINE 7/18/2025, 2:05:14 PM 7/18/2025, 2:05:14 PM 10.183.96.1 Mvc01
- log in into
MvcCore02- goto
https://bbb.xxxxxxx.zzz:7201/home/DoLogin - check the sessions. Now we have two lines
- goto
testuser REGULAR 7/18/2025, 2:09:46 PM 7/18/2025, 2:09:46 PM 10.183.96.1 Mvc02
testuser OFFLINE 7/18/2025, 2:05:14 PM 7/18/2025, 2:05:14 PM 10.183.96.1 Mvc01
- logout from the second app
- goto
https://bbb.xxxxxxx.zzz:7201/home/DoLogout - check the sessions. One line is still present:
- goto
testuser OFFLINE 7/18/2025, 2:05:14 PM 7/18/2025, 2:05:14 PM 10.183.96.1 Mvc01
- logout from the first app
- goto
https://aaa.xxxxxxx.zzz:7008/home/DoLogout - check the sessions. One line is still present (!!!)
- we expected that the session would be deleted
- goto
testuser OFFLINE 7/18/2025, 2:05:14 PM 7/18/2025, 2:05:14 PM 10.183.96.1 Mvc01
- add a refernce onto
Duende.AccessTokenManagement.OpenIdConnect- your MvcCore01.csproj become as follows
Click to show the code
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Duende.AccessTokenManagement.OpenIdConnect" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.17" />
</ItemGroup>
</Project>- modify Program.cs as follows
Click to show the code
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace MvcCore01
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenIdConnectAccessTokenManagement(o =>
{
// o.ClientCredentialsScope = "openid profile offline_access";
// o.ClientCredentialsResource = "OidcWebApiResource OidcWebApiIntrospectionResource";
// o.DPoPJsonWebKey = "jwk";
});
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options => {
options.Events.OnSigningOut = async e =>
{
await e.HttpContext.RevokeRefreshTokenAsync();
};
}
)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "SSO", o =>
{
//o.SignInScheme = IdentityConstants.ExternalScheme;
//o.SignOutScheme = OpenIdConnectDefaults.AuthenticationScheme; // IdentityConstants.ApplicationScheme;
o.ClientId = "Mvc01";
o.ClientSecret = "8JQRgt9U7kJKhl7HGBgQ6rhH6WlmU94p";
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");
/// <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 = "/"
// 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");
o.ResponseType = OpenIdConnectResponseType.Code;
o.UsePkce = true;
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;
});
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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.UseAuthentication(); // Added
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
}
}- after logout of the first app
OFFLINE-session will be deleted in the list of Keycloak sessions
- We are going to reference this note in the next article (Keycloak
id_token_hintissue):- check the browser cookies after log in to the second app
-
.AspNetCore.Cookies=chunks-2 -
.AspNetCore.CookiesC1=... -
.AspNetCore.CookiesC2=...
-
- check the browser cookies after log in to the first app (it requests
offline_access-scope)-
.AspNetCore.Cookies=chunks-3 -
.AspNetCore.CookiesC1=... -
.AspNetCore.CookiesC2=... -
.AspNetCore.CookiesC3=...
-
- check the browser cookies after log in to the second app
- take a closer look at the logout method
- it can be like this
public async Task DoLogout()
{
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
- or it can be like this
public IActionResult DoLogout()
{
return new SignOutResult(
new[] {
OpenIdConnectDefaults.AuthenticationScheme,
// CookieAuthenticationDefaults.AuthenticationScheme // do not insert this line
});
}
- if we use the code like below
public IActionResult DoLogout()
{
return new SignOutResult(
new[] {
OpenIdConnectDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme // do not insert this line
});
}
-
we obtain the Keycloak message:
Missing parameters: id_token_hint -
if we change the code like below
public IActionResult DoLogout()
{
return new SignOutResult(
new[] {
// OpenIdConnectDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme // do not insert this line
});
}
- app logs out without errors
- but the next time you log in, the application will not ask for your username and password (!!!)
- It is the same as to login two or more times without logout. The very first time it will ask for Username and password and at the second time it will not.
- OpenIdConnect accepts user credentials from a Keycloak session until the session expires and does not ask for username and password.
- Bad user experience: After your first login, you will not be able to log in under a different username.
- require remembering
https://kc.rupbes.by:8445/realms/rupbes.test/account-ref
- require remembering
- Security Issue: Once you log out, anyone can log into your account without a password on the same workstation.
- Bad user experience: After your first login, you will not be able to log in under a different username.
- but the next time you log in, the application will not ask for your username and password (!!!)