WebApp MVC
An ASP.NET Core Web app signing-in users with Keycloak.AuthServices
Scenario
This sample shows how to build a .NET Core MVC Web app that uses OpenID Connect to sign in users. In a typical cookie-based authentication scenario using OpenID Connect, the following components and flow are involved:
Components:
- User: The end-user who wants to authenticate.
- Client: The application that wants to authenticate the user (e.g., a web application).
- Authorization Server: The server that performs the authentication and issues tokens (e.g., Google, Facebook).
- Resource Server: The server hosting the protected resources that the client wants to access.
Authentication Flow:
- Step 1: The user accesses the client application and requests to log in.
- Step 2: The client redirects the user to the authorization server's login page, where the user enters their credentials.
- Step 3: Upon successful authentication, the authorization server redirects the user back to the client application with an authorization code.
- Step 4: The client exchanges the authorization code for an ID token and an access token at the authorization server's token endpoint.
- Step 5: The authorization server validates the authorization code, and if valid, issues the ID token and access token.
- Step 6: The client validates the ID token and retrieves the user's identity information.
- Step 7: The client creates a session for the user and stores the session identifier in a secure, HTTP-only cookie.
- Step 8: The user's subsequent requests to the client include the session cookie, which the client uses to identify the user and maintain the authenticated session.
- Step 9: If the client needs to access protected resources from a resource server, it can use the access token to authenticate the requests.
Role Mapping
WARNING
By default Keycloak doesn't map roles to id_token, so we need an access_token in this case, access_token is NOT available in all OAuth flows. For example, "Implicit Flow" is based on id_token and access_token is not retrieved at all.
Code
cs
using Keycloak.AuthServices.Authentication;
using Keycloak.AuthServices.Authorization;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
});
builder
.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddKeycloakWebApp(
builder.Configuration.GetSection(KeycloakAuthenticationOptions.Section),
configureOpenIdConnectOptions: options =>
{
// we need this for front-channel sign-out
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.Events = new OpenIdConnectEvents
{
OnSignedOutCallbackRedirect = context =>
{
context.Response.Redirect("/Home/Public");
context.HandleResponse();
return Task.CompletedTask;
}
};
}
);
builder
.Services.AddKeycloakAuthorization(builder.Configuration)
.AddAuthorizationBuilder()
.AddPolicy("PrivacyAccess", policy => policy.RequireRealmRoles("Admin"));
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}")
.RequireAuthorization();
app.MapRazorPages();
app.Run();
cs
namespace WebApp_OpenIDConnect_DotNet.Controllers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
public class AccountController : Controller
{
private readonly ILogger<AccountController> logger;
public AccountController(ILogger<AccountController> logger) => this.logger = logger;
[AllowAnonymous]
public IActionResult SignIn()
{
if (!this.User.Identity!.IsAuthenticated)
{
return this.Challenge(OpenIdConnectDefaults.AuthenticationScheme);
}
return this.RedirectToAction("Index", "Home");
}
[AllowAnonymous]
public async Task<IActionResult> SignOutAsync()
{
if (!this.User.Identity!.IsAuthenticated)
{
return this.Challenge(OpenIdConnectDefaults.AuthenticationScheme);
}
var idToken = await this.HttpContext.GetTokenAsync("id_token");
var authResult = this
.HttpContext.Features.Get<IAuthenticateResultFeature>()
?.AuthenticateResult;
var tokens = authResult!.Properties!.GetTokens();
var tokenNames = tokens.Select(token => token.Name).ToArray();
this.logger.LogInformation("Token Names: {TokenNames}", string.Join(", ", tokenNames));
return this.SignOut(
new AuthenticationProperties
{
RedirectUri = "/",
Items = { { "id_token_hint", idToken } }
},
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme
);
}
[AllowAnonymous]
public IActionResult AccessDenied() => this.RedirectToAction("AccessDenied", "Home");
}
See sample source code: keycloak-authorization-services-dotnet/tree/main/samples/WebApp