Skip to content

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