Neovim Clipboard on WSL: The One-Liner Fix

Every time I set up Neovim on a fresh WSL instance, I hit the same wall: yanking text inside Neovim and pasting it into a Windows app (or vice versa) just doesn't work. "+y does nothing, and Neovim greets you with Clipboard: No provider, try :checkhealth. Nothing flows in or out of the clipboard, not even between files inside WSL.

The root cause is that WSL's Neovim can't talk to the Windows clipboard at all. The fix is a tiny Windows executable called win32yank that speaks the Windows clipboard API from the command line.

I've done this enough times now that I'm writing it down so I never have to search for it again. If you're here for the same reason, this one's for you.

Step-by-Step

1. Download win32yank

Grab the latest release from github.com/equalsraf/win32yank. Download win32yank-x64.zip and extract it to get win32yank.exe.

2. Place it in your WSL PATH

sudo mv /mnt/d/win32yank.exe /usr/local/bin/

Adjust the source path to wherever your browser downloaded it (usually /mnt/c/Users/<you>/Downloads/win32yank.exe).

3. Configure Neovim

Add this block to ~/.config/nvim/init.lua:

if vim.fn.has("wsl") == 1 then
  vim.g.clipboard = {
    name = 'win32yank-wsl',
    copy = {
      ['+'] = 'win32yank.exe -i --crlf',
      ['*'] = 'win32yank.exe -i --crlf',
    },
    paste = {
      ['+'] = 'win32yank.exe -o --lf',
      ['*'] = 'win32yank.exe -o --lf',
    },
    cache_enabled = 0,
  }
  vim.opt.clipboard = 'unnamedplus'
end

4. Done

Now y, "+y, "+p, right-click copy/paste — all of it flows through the Windows clipboard as you'd expect.



Bonus: One-Shot Setup Script

Next time I (or you) need this on a fresh box, run this single script. It downloads win32yank, installs it, and appends the config:

#!/usr/bin/env bash
set -euo pipefail

WIN32YANK_PATH="/usr/local/bin/win32yank.exe"
NVIM_CONFIG="${HOME}/.config/nvim/init.lua"
TMP_DIR=$(mktemp -d)

# Get the latest release tag from GitHub
echo "==> Fetching latest win32yank release..."
LATEST_TAG=$(curl -s https://api.github.com/repos/equalsraf/win32yank/releases/latest \
  | grep '"tag_name"' \
  | cut -d'"' -f4)

echo "==> Downloading win32yank ${LATEST_TAG}..."
curl -fsSL "https://github.com/equalsraf/win32yank/releases/download/${LATEST_TAG}/win32yank-x64.zip" \
  -o "${TMP_DIR}/win32yank-x64.zip"

echo "==> Extracting..."
unzip -q "${TMP_DIR}/win32yank-x64.zip" -d "${TMP_DIR}"
sudo cp "${TMP_DIR}/win32yank.exe" "$WIN32YANK_PATH"
sudo chmod +x "$WIN32YANK_PATH"
rm -rf "$TMP_DIR"

echo "==> Appending clipboard config to ${NVIM_CONFIG}..."
mkdir -p "$(dirname "$NVIM_CONFIG")"

cat >> "$NVIM_CONFIG" << 'LUA'

-- win32yank clipboard for WSL
if vim.fn.has("wsl") == 1 then
  vim.g.clipboard = {
    name = 'win32yank-wsl',
    copy = {
      ['+'] = 'win32yank.exe -i --crlf',
      ['*'] = 'win32yank.exe -i --crlf',
    },
    paste = {
      ['+'] = 'win32yank.exe -o --lf',
      ['*'] = 'win32yank.exe -o --lf',
    },
    cache_enabled = 0,
  }
  vim.opt.clipboard = 'unnamedplus'
end
LUA

echo "==> Done! Restart Neovim and yank away."

Save it as setup-wsl-clipboard.sh, run chmod +x setup-wsl-clipboard.sh and then ./setup-wsl-clipboard.sh.

Reading Notes #702

This week’s compilation explores a mix of critical topics ranging from integrating AI models with SQL Server to navigating the complexities of Azure container troubleshooting. I’ve selected these particular articles because they offer practical ways to streamline your workflow and better understand the latest shifts in cloud infrastructure and software development.

Databases

AI

Cloud

DevOps

Miscellaneous

Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, and books that catch my interest during the week. 

 ~frank

Reading Notes #701

Modern infrastructure and AI agent development move fast, requiring a sharp eye on both backend stability and frontend polish. This week’s curated notes highlight critical shifts in observability, security best practices for autonomous agents, and practical updates for .NET MAUI apps. Check out these essential reads to stay ahead of the curve.


AI

Programming

DevOps

  • Why Choose Pulumi Over Terraform? (Pablo Seibelt) - I have never tried Pulumi or terraform, but they look great, and after reading this post, I'm very interested in learning more about ballooning

Data

MS Build


~frank

Reading Notes #700

Seven hundred weeks.

When I started taking notes about the articles I was reading, I never imagined I would still be doing it 700 weeks later.


Back then, my notes lived on a USB key. I carried a small personal wiki with me and used it to save interesting articles, ideas, and discoveries. It was a simple way to build my own searchable knowledge base so I could find things again when I needed them.

In 2011, I started sharing those notes publicly on my blog, Franky's Notes. A few months later, I made another important change: I switched from writing in French to writing in English. At the time, I wasn't fluent, but I wanted to improve. "Notes de lecture" became "Reading Notes", and every week became an opportunity to learn something new while practicing a language that would eventually become a big part of my career.

Over the years, the format evolved. Articles were joined by podcasts, books, videos, and whatever else helped me learn and stay curious. Technology changes constantly, and one of the things I enjoy most about working in this industry is that there is always something new to discover.

What never changed was the habit itself.

Most mornings start the same way: a coffee, my e-reader, and a few articles. Throughout the week, I collect the things that made me think, taught me something, or simply felt worth sharing. Then, every Monday, I publish a new edition.

Seven hundred weeks later, these reading notes have become much more than a list of links. They are a record of what caught my attention, what I was learning, and how both technology and I have changed over the years.

If you've been reading along for a while, thank you. If you're new here, I hope you discover something interesting in the links below.

Suggestion of the week

AI

Programming

Miscellaneous

~frank

Reading Notes #699

This week's reading notes bring you the latest insights into AI, .NET, open-source development, and even a few social hacks! From exploring background tasks in Blazor to the fascinating debate on Markdown vs. HTML for AI output, this roundup has something for everyone.

Jean-Olivier P. presenting at MsDevMtl user group

Let me know if you find anything particularly interesting; I'd love to hear your thoughts!

Programming

AI

Open Source

Podcasts

Miscellaneous


Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, and books that catch my interest during the week. 

 ~frank

Reading Notes #698

The world of AI is exploding, and with that explosion comes a crucial question: how do we keep these powerful agents in check? Traditional security methods might not cut it anymore, so developers are turning to innovative sandboxing techniques. Let's explore some of the most promising approaches and see which ones emerge as the frontrunners in this AI safety race.




AI

Programming

DevOps

Podcasts


I've made it a habit to share the fascinating articles, blog posts, and books that cross my path each week. Think of this as an open invitation, if you stumble upon something intriguing, don't hesitate to share it!
Let's build a community of curious minds.

~frank

Reading Notes #697

This week’s reading notes cover a wide range of topics, from local AI workflows and Docker agent fleets to data privacy, SQL tips, and developer tooling updates. There’s also an interesting look at how AI may be reshaping platforms like GitHub, alongside practical articles and podcasts packed with ideas for developers and tech enthusiasts alike.


Programming

Data

AI

Databases

Podcasts


Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, and books that catch my interest during the week.

If you have interesting content, share it!

~frank

Apps That See: Bringing Vision AI to Your Projects

I was wearing a t-shirt with a partial Reka logo at the edge of the frame. I never said the word "Reka" in that segment. The model caught the logo, connected it to the topic I was discussing, and mentioned it unprompted in the output it generated.

That is not a transcript trick. The model was watching.

At the AI Agents Conference 2026, I gave a talk called "Apps That See" — six live demos showing how to build applications that understand images and video. Every project is open source and ready to clone. This post walks through each one so you have enough context to pick it up, run it, and adapt it to something useful in your own work.

Vision AI Is Accessible Now

Not long ago, working with visual AI meant GPU clusters, specialized teams, and weeks of training. Today a compressed 4B model like Qwen or Gemini 3 runs on a regular laptop and handles image description well enough to prototype. Step up to a 7B model like Reka Edge and the quality improves meaningfully. It also runs locally: a gaming PC with a decent GPU is enough. No server required.

For tasks that need more power, cloud APIs give you faster results without local hardware requirements. The tradeoff is that your images and video go to a third-party provider. For corridor cameras or stock photos that is usually acceptable. For private or sensitive content, local is the better default.

The practical pattern: start local to build and test, then decide whether the task actually requires cloud.

What You Can Build With This

  • Accessibility: Describe a scene in real time for visually impaired users, or identify objects on demand.
  • Content creation: Extract structure from a video and turn it into a blog post, caption set, or highlight reel.
  • Productivity: Search through thousands of videos for a specific object or topic, even when the title gives no indication of the content.
  • Automation: Trigger actions only when specific visual conditions are met, such as an unrecognized person entering a room.
  • Fun: Most developers' first contact with AI is building something for themselves, and that is a perfectly valid starting point.


Demo 1: Caption This — Generate a Prompt from Any Image


Source: fboucher/caption-this

If you work with image generation models, you end up with a lot of images to test and compare. Writing the text prompt that would reproduce a specific image is tedious. This tool does it for you: give it an image, get back a prompt you can use to regenerate something similar.

The demo uses an HTTP client extension in VS Code to call the API directly, no SDK. Pass an image, ask for a plain-text prompt that would recreate it. One prompt detail that improved results noticeably: add no markdown to the instruction.

POST https://api.reka.ai/v1/chat
Content-Type: application/json

{
  "model": "reka-flash",
  "messages": [{
    "role": "user",
    "content": [
      { "type": "image_url", "image_url": { "url": "https://..." } },
      { "type": "text", "text": "Write a prompt in plain text, no markdown, that would generate the exact same image." }
    ]
  }]
}

One thing to know when testing this across different models: some accept an image URL directly, others require the image as a base64-encoded string. Same task, same prompt, different input contract. If you plan to swap models in your app, account for this difference from the start.

Demo 2: Media Library — Compare Vision Models Side by Side


Source: fboucher/media-library

This is a web app that connects to multiple vision backends and lets you switch between them at runtime. The motivation: benchmark Reka Edge running locally — via OpenRouter or directly through the Reka API — against other models on real tasks.

Object detection surfaces the biggest portability problem. Some models return bounding boxes in an HTML-style bracket format with pixel coordinates. Others use a 2D box structure with a different coordinate scheme. If you code against one format and then swap models, your rendering breaks. There is no standard here — handle the differences at the application layer, not the model layer.

The app uses the OpenAI API format as the common interface across all backends. Any model with a compatible endpoint can be swapped in with minimal changes. It does not eliminate the per-model quirks, but it reduces the friction of switching to a configuration change rather than a rewrite.

Video input is supported too, though far fewer models handle it than images. Of the models tested, Reka Edge is the standout for video — the others either reject it or behave inconsistently.

Demo 3: Video2Blog — Turn a Video into a Structured Post


Source: fboucher/video2blog

I built this for myself. I do a lot of tutorial videos and I wanted a tool that would turn a recording into a structured blog post without me having to write one from scratch.

The tool sends the video to a vision model with a detailed prompt: target structure, tone, format, and an instruction to flag moments where a screenshot would add value. The model returns timestamps — it cannot extract frames itself, but it tells you exactly where to look, and you pull them locally with ffmpeg.

That creates one architectural quirk worth knowing: the video lives in two places. ffmpeg needs it locally to extract frames. The hosted model needs it uploaded to analyze content. For a one-evening project it works well enough, and I use it often enough that it has paid for itself many times over.

After the first draft, you stay in a conversation loop: change the tone, translate to French, swap a timestamp, restructure a section. The model holds context and iterates with you until the result is what you want.

Demo 4: Video Analyzer — Search and Query Your Video Library


Source: reka-ai/api-examples-dotnet

Most video search runs on titles, descriptions, and transcribed audio. This demo searches by what is actually visible on screen.

The app pre-indexes a video library by sending each video through a vision model ahead of time. When a query arrives, the heavy work is already done. A search for "robot arm" returns the right video — a clip of a robotic arm animation. It also returns a false positive: fast-moving hands apparently looked close enough to fool the model. Useful, not perfect, and worth designing around in your UX.

The Q&A feature goes further. You pick a video and ask a specific question. "What database was used?" returned MySQL — and noted it was running in a Docker container. The model identified that from watching the screen, not from audio. No transcript needed.

From there, you can generate study materials from any recorded session. The demo produces a multiple-choice quiz with answer options, correct answers, and explanations. The model is doing comprehension, not transcription.

Demo 5: Roast My Life — What the Model Actually Sees


Source: reka-ai/api-examples-python

I never mentioned the pictures on my wall. The model did.

In a video about Python and AI, the model's generated blog post made a remark about the artwork hanging behind me. I had said nothing about it. The model noticed, mentioned it, and moved on as if it were obvious.

Then there was the t-shirt moment described at the top of this post. A partial logo, half out of frame, no mention of it anywhere in the audio — and the model connected it to the topic anyway.

This demo is named Roast My Life because the model ends up commenting on things you never intended to share. But the real point is what it reveals: a vision model is not a smarter transcript. It is watching. The larger models do this particularly well, and once you see it, it changes how you think about what these tools can do — and what they will pick up without you asking.

Demo 6: N8N Automation — No-Code Video Clipping Pipeline


Sources: N8N Reka Vision integration

Vision AI does not always need custom code. This demo wires everything together in N8N, a visual workflow tool, with no programming required.

The trigger is a new video published to YouTube. The workflow finds an engaging clip, reformats it from horizontal to vertical, adds captions in a specific style (all lowercase, specific colors — chosen to be obviously distinct from any default), and sends an email with the finished clip attached. The whole thing runs automatically.

For developers, this pattern is worth knowing even if you code everything else. Many real business workflows have a vision AI step that fits cleanly into a larger automation, and a no-code tool is often the fastest way to ship it.


Watch the Full Talk

The demos above are the written version. The live version, with the actual code running, models responding in real time, and a few things going sideways in interesting ways, is on YouTube.


All the Code

The demos span Python, C#, raw HTTP, Go, and N8N. Vision AI is not tied to a specific stack — if your environment can make an HTTP request, it can call a vision model.

All projects:


Reading Notes #696

This week's collection highlights the rapid evolution of AI agents, exploring their asynchronous capabilities, deployment journeys, and their impact on DevOps and video editing. On the programming front, we explore new Git features and API versioning with OpenAPI in .NET 10. We also dive into some fascinating podcast discussions ranging from the GUI vs. CLI debate to generational perspectives in the workplace. 
Enjoy the reading!

AI

Programming

Podcast


~frank

Reading Notes #695

A mix of thoughtful perspectives and practical updates this week. From evolving AI tools and model selection guidance to changes in developer workflows and tooling, there’s plenty to reflect on. Add in insights on streaming and a strong push toward more secure environments, and you get a well-rounded set of reads worth your time.


Suggestion of the week

AI

Programming

Miscellaneous

  • Livestreaming Before It Was Cool (Golnaz) - Curious to learn more about the streaming options from the different platforms to the tools, and the pro and cons of each? This post is for you, and on top of that, you get the Microsoft story.
~frank


Reading Notes #694

A fast-moving mix this week: AI tooling, ARM readiness, Docker sandboxes, and real-world lessons from agents. Practical insights across .NET, DevOps, and local-first workflows.


Suggestion of the week

AI

Programming

DevOps

Podcasts

  • Our Favorite Agent Setups (Agentic DevOps) - Nice discussion that goes through many AI harnesses, agents, models, and what they are playing with right now. OpenClaw, OpenCode, Claude Code, Copilot, and all of it.

  • Michael Perry: AI-assisted Development - Episode 397 (AI DevOps Podcast) - Interesting discussion about AI-assisted Development (or can we say programming?) with a focus on skills and how they could be defined.


Reading Notes #693

I'm always on the lookout for innovative ways to enhance my coding experience, and this week's Reading Notes are filled with exciting discoveries! From cutting-edge UI libraries to secure sandbox environments for AI agents, I've curated a selection of articles that showcase the latest programming trends and technologies. 

Whether you're interested in harnessing the power of Docker sandboxes or exploring the potential of smart glasses integration, there's something on this list for everyone.


Programming

AI

Miscellaneous

~frank

How to Serve a Vision AI Model Locally with vLLM and Reka Edge

Running an AI model as a one-shot script is useful, but it forces you to restart the model every time you need a result. Setting it up as a service lets any application send requests to it continuously, without reloading the model. This guide shows how to serve Reka Edge using vLLM and an open-source plugin, then connect a web app to it for image description and object detection.

Prerequisites

You need a machine with a GPU and either Linux, macOS, or Windows (with WSL). I use UV, a fast Python package and project manager, or pip + venv if you prefer.

Clone the vLLM Reka Plugin

Reka models require a dedicated plugin to run under vLLM, not all models need this extra step, but Reka's architecture requires it. Clone the plugin repository and enter the directory:

git clone https://github.com/reka-ai/vllm-reka
cd vllm-reka

The repository contains the plugin code and a serve.sh script you will use to start the service.

Download the Reka Edge Model

Before starting the service, you need the model weights locally. Install the Hugging Face Hub CLI and use it to pull the reka-edge-2603 model into your project directory:

uv sync
uv pip install huggingface_hub
uvx hf download RekaAI/reka-edge-2603 --local-dir ./models/reka-edge-2603

This is a large model, so make sure you have enough disk space and a stable connection.

Start the Service

Once the model is downloaded, start the vLLM service using the serve.sh script included in the plugin:

uv run bash serve.sh ./models/reka-edge-2603

The script accepts environment variables to configure which model to load and how much GPU memory to allocate. If your GPU cannot fit the model at default settings, open serve.sh and adjust the variables at the top. The repository README lists the available options. The service takes a few seconds to load the model weights, then starts listening for HTTP requests.

As an example with an NVIDIA GeForce RTX 5070, here are the settings I used to run the model:

export GPU_MEM=0.80
export MAX_LEN=4096
export MAX_BATCH_TOKENS=4096
export MAX_IMAGES=2
export MAX_VIDEOS=1
export VIDEO_NUM_FRAMES=4
uv run bash serve.sh ./models/reka-edge-2603

Connect the Media Library App

With the backend running, time to start the Media Library app. Clone the repository, jump into the directory, and run it with Docker:

git clone https://github.com/fboucher/media-library
cd media-library
docker compose up --build -d

Open http://localhost:8080 in your browser, then add a new connection with these settings:

  • Name: local (or any label you want)
  • IP address: your machine's local network IP (e.g. 192.168.x.x)
  • API key: leave blank or enter anything — no key is required for a local connection
  • Model: reka-edge-2603

Click Test to confirm the connection, then save it.


Try It: Image Description and Object Detection

Select an image in the app and choose your local connection, then click Fill with AI. The app sends the image to your vLLM service, and the model returns a natural language description. You can watch the request hit your backend in the terminal where the service is running.

Reka Edge also supports object detection. Type a prompt asking the model to locate a specific feature (ex: "face") and the model returns bounding-box coordinates. The app renders these as red boxes overlaid on the image. This works for any region you can describe in a prompt.



Switch to the Reka Cloud API

If your local GPU is too slow for production use, you can point the app at the Reka APIs instead. Add a new connection in the app and set the base URL to the Reka API endpoint. Get your API key from platform.reka.ai. OpenRouter is another option if you prefer a unified API across providers.

The model name stays the same (reka-edge-2603), so switching between local and cloud is just a matter of selecting a different connection in the app. The cloud API is noticeably faster because Reka's servers are more powerful than a local GPU (at least mine :) ). During development, use the local service to avoid burning credits; switch to the API for speed when you need it.

What You Can Build

The service you just set up accepts any image, or video via HTTP — point a script at a folder and you have a batch pipeline for descriptions, tags, or bounding boxes. Swap the prompt and you change what it extracts. The workflow is the same whether you are running locally or through the API.

References

Reading Notes #692

The tech landscape is constantly evolving, and keeping up with the latest developments can be overwhelming. From AI-powered tools like Ollama and OpenClaw, to new ways of programming with Aspire Docs and Azure CLI, it seems like there's always something new to explore. In this edition of Reading Notes, I'll share some of the interesting things that caught my eye recently, from AI advancements to developer tools and beyond.


Suggestion of the week

AI

Programming

Cloud

Miscellaneous


Sharing my Reading Notes is a habit I started a long time ago, where I share a list of all the articles, blog posts, podcasts and books that catch my interest during the week.

If you have interesting content, share it!

~frank

Adding Keycloak Authentication to an Existing .NET Aspire Application

By the end of this post, you'll have a working login/logout flow backed by Keycloak, running locally via Aspire and deployable via Docker Compose.

If your Aspire app doesn't have authentication yet, this is your fastest path to a real identity provider. This tutorial walks through wiring Keycloak OIDC into an existing .NET Aspire + Blazor Server app: from AppHost registration to login/logout UI, using production code from NoteBookmark, an open-source bookmark manager built with .NET Aspire and Blazor.\

[version en français disponible]

Step 1: Add Aspire.Hosting.Keycloak to AppHost

Aspire provides first-class Keycloak support through the Aspire.Hosting.Keycloak package. Add it to your AppHost project:

For AppHost project

dotnet add package Aspire.Hosting.Keycloak

Run dotnet restore to pull the package.


Step 2: Register Keycloak in AppHost.cs

With the package installed, register Keycloak as a resource in your AppHost. Aspire will spin up a Keycloak container, wire its connection details into dependent projects, and ensure proper startup ordering.


// ...

// Add Keycloak authentication server
var keycloak = builder.AddKeycloak("keycloak", port: 8080)
    .WithDataVolume(); // Persist Keycloak data across container restarts

if (builder.Environment.IsDevelopment())
{
    // ...

    builder.AddProject<NoteBookmark_BlazorApp>("blazor-app")
        // ...
        .WithReference(keycloak)  // <-- reference Keycloak
        .WaitFor(keycloak)  // <-- wait for Keycloak to be ready
        .WithExternalHttpEndpoints()
        .PublishAsDockerComposeService((resource, service) =>
        {
            service.ContainerName = "notebookmark-blazor";
        });
}

Key Changes:

  • AddKeycloak("keycloak", port: 8080): Registers a Keycloak resource listening on port 8080.
  • WithDataVolume(): Persists Keycloak's configuration and realm data across container restarts. Without this, you'd lose your realm setup every time the container stops.
  • .WithReference(keycloak): Injects Keycloak connection settings (base URL, etc.) into the BlazorApp as environment variables.
  • .WaitFor(keycloak): Ensures Keycloak is fully started before launching the Blazor app. This is critical: if your app starts before Keycloak is ready, OIDC discovery will fail.

 

Step 3: Set Up Keycloak for Non-Aspire (aka prod) Deployments

This post focuses on the Aspire dev setup, but for production (Docker Compose, Kubernetes), you need a standalone Keycloak. Here's what that looks like and why.

Aspire can actually help you bridge the gap. AddDockerComposeEnvironment() in AppHost generates a draft Docker Compose file from your Aspire model, a great starting point before customizing for production. Worth checking out if you want a head start.

The final compose files for both Keycloak and the NoteBookmark app are available in the NoteBookmark repo:

A few things worth noting about the setup:

  • Postgres as the backing store: Keycloak uses a dedicated Postgres instance (not the app's database) to persist realm configuration, users, and sessions.
  • KC_HTTP_ENABLED: "true": Allows HTTP traffic internally. In production, Keycloak runs behind a reverse proxy (nginx, Traefik) that handles TLS termination: HTTPS externally, HTTP internally.
  • KC_FEATURES: "token-exchange": Enables the token exchange feature, needed if you want service-to-service auth flows.

Step 4: Configure Keycloak Realm and OIDC Client

This configuration is required in both production and development environments, but only needs to be done once per environment. In dev, thanks to .WithDataVolume(), all Keycloak settings are persisted between run and debug sessions, so you only configure it once and it survives restarts.

Once Keycloak is running, configure it:

  1. Navigate to http://localhost:8080 and log in with your admin credentials.
  2. Create a new realm:
    • Click Create Realm
    • Name: notebookmark (match the realm in your Authority URL below)
  3. Create an OIDC client:
    • Clients → Create Client
    • Client ID: notebookmark
    • Client Protocol: openid-connect
    • Access Type: confidential (generates a client secret)
    • Valid Redirect URIs: http://localhost:5173/* (adjust for your Blazor app's URL)
    • Web Origins: http://localhost:5173
  4. Go to the Credentials tab and copy the Client Secret: you'll need this in your app config.
Keycloak client configuration screen


Step 5: Add OpenID Connect to the Blazor App

Now wire up the authentication pipeline in your Blazor Server app.

Add the NuGet Package


For BlazorApp project

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Update Program.cs

BlazorApp/Program.cs:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

//...

// Add authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    var authority = builder.Configuration["Keycloak:Authority"];
    options.Authority = authority;
    options.ClientId = builder.Configuration["Keycloak:ClientId"];
    options.ClientSecret = builder.Configuration["Keycloak:ClientSecret"];
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;

    // Allow overriding RequireHttpsMetadata via configuration.
    // Relax the requirement when running in a container against HTTP Keycloak.
    var requireHttpsConfigured = builder.Configuration.GetValue<bool?>("Keycloak:RequireHttpsMetadata");
    var isRunningInContainer = string.Equals(
        System.Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"),
        "true",
        StringComparison.OrdinalIgnoreCase);

    if (requireHttpsConfigured.HasValue)
    {
        options.RequireHttpsMetadata = requireHttpsConfigured.Value;
    }
    else
    {
        var defaultRequireHttps = !builder.Environment.IsDevelopment();
        if (isRunningInContainer &&
            !string.IsNullOrEmpty(authority) &&
            authority.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
        {
            defaultRequireHttps = false;
        }
        options.RequireHttpsMetadata = defaultRequireHttps;
    }

    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");

    options.TokenValidationParameters = new()
    {
        NameClaimType = "preferred_username",
        RoleClaimType = "roles"
    };

    // Configure logout to pass id_token_hint to Keycloak
    options.Events = new OpenIdConnectEvents
    {
        OnRedirectToIdentityProviderForSignOut = async context =>
        {
            var idToken = await context.HttpContext.GetTokenAsync("id_token");
            if (!string.IsNullOrEmpty(idToken))
            {
                context.ProtocolMessage.IdTokenHint = idToken;
            }
        }
    };
});

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();

// ... existing Razor Components, FluentUI, etc. ...

var app = builder.Build();
app.MapDefaultEndpoints();

// ... existing middleware ...

// CRITICAL: UseAuthentication BEFORE UseAuthorization
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

// Authentication endpoints
app.MapGet("/authentication/login", async (HttpContext context, string? returnUrl) =>
{
    var authProperties = new AuthenticationProperties { RedirectUri = returnUrl ?? "/" };
    await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
});

app.MapGet("/authentication/logout", async (HttpContext context) =>
{
    var authProperties = new AuthenticationProperties { RedirectUri = "/" };
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
});

app.Run();


Configuration


Create or update appsettings.json in the BlazorApp project:

{
  "Keycloak": {
    "Authority": "http://localhost:8080/realms/notebookmark",
    "ClientId": "notebookmark",
    "ClientSecret": "your-client-secret-from-keycloak",
    "RequireHttpsMetadata": false
  }
}

For your Prod, Docker Compose deployments, use environment variables in your docker-compose.yaml:

environment:
  Keycloak__Authority: ${KEYCLOAK_AUTHORITY}
  Keycloak__ClientId: ${KEYCLOAK_CLIENT_ID}
  Keycloak__ClientSecret: ${KEYCLOAK_CLIENT_SECRET}
In the development enviroment, Aspire's .WithReference(keycloak) automatically injects environment variables like services__keycloak__http__0 for the Keycloak base URL. You can read this in your config or manually set the Authority URL as shown above.

 

Handling HTTP vs HTTPS: The RequireHttpsMetadata Gotcha

By default, the OpenID Connect middleware requires HTTPS for metadata discovery (RequireHttpsMetadata = true). This is a security best practice for production, but it causes problems in local/container dev environments where Keycloak runs on HTTP.

The code above implements a smart fallback:

  1. Check explicit configuration first: If Keycloak:RequireHttpsMetadata is set in config, use that value.
  2. Detect container environment: If running in a container (DOTNET_RUNNING_IN_CONTAINER=true) and the Authority URL is HTTP, disable the HTTPS requirement.
  3. Default to HTTPS in production: Outside of Development mode, default to requiring HTTPS.

This ensures:

  • Local dev works seamlessly with HTTP Keycloak
  • Container-to-container communication works (HTTP internally)
  • Production enforces HTTPS (assuming you've configured it properly)

Note: In production, run Keycloak behind a reverse proxy (nginx, Traefik, etc.) that handles TLS termination. Your app sees https://yourdomain.com, Keycloak internally runs on HTTP.

That's the server-side setup done. Now let's build the Blazor UI pieces that make auth visible to users.


Step 6: Blazor UI: Login, Logout, and Route Protection

With the backend authentication pipeline configured, it's time to build the UI components that let users actually log in, log out, and interact with protected content. We'll create three key pieces: the login/logout pages, a login display component, and routing configuration that enforces authorization.

Keycloak login screen

The Login and Logout Razor Pages

First, we need pages to trigger authentication flows. These aren't typical Blazor pages with markup—they're redirect triggers that hand off control to Keycloak.

Login.razor

Create Components/Pages/Login.razor:

@page "/login"
@attribute [AllowAnonymous]
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
@inject NavigationManager Navigation
@inject IHttpContextAccessor HttpContextAccessor

@code {
    protected override async Task OnInitializedAsync()
    {
        var uri = new Uri(Navigation.Uri);
        var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
        var returnUrl = query["returnUrl"] ?? "/";

        var httpContext = HttpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            var authProperties = new AuthenticationProperties
            {
                RedirectUri = returnUrl
            };
            await httpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
        }
    }
}

What's happening here?

  • No markup: This page doesn't render anything. Its job is to initiate the OpenID Connect authentication challenge, which redirects the browser to Keycloak.
  • ChallengeAsync: This triggers the OIDC middleware to redirect the user to Keycloak's login page.
  • Return URL: We capture the returnUrl query parameter so users land back where they started after logging in.
  • [AllowAnonymous]: Critical! Without this, the page would require authentication to access, creating a redirect loop.

Logout.razor

Create Components/Pages/Logout.razor:

@page "/logout"
@attribute [AllowAnonymous]
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
@inject IHttpContextAccessor HttpContextAccessor

@code {
    protected override async Task OnInitializedAsync()
    {
        var httpContext = HttpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            var properties = new AuthenticationProperties
            {
                RedirectUri = "/"
            };
            await httpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, properties);
            await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}


Why sign out of TWO schemes?


This is where many implementations fail. OpenID Connect uses a dual authentication scheme:

  1. OpenIdConnect scheme: Handles the protocol dance with Keycloak (redirects, token exchange, logout).
  2. Cookie scheme: Manages the local session in your Blazor app.

When logging out, you must sign out of both, in this order:

  1. OIDC first: This redirects to Keycloak's logout endpoint, ending the SSO session.
  2. Cookie second: This clears the local authentication cookie.

Signing out of only the cookie leaves the Keycloak session active—users can click "Login" and get back in without re-entering credentials. Signing out only from OIDC leaves the local cookie intact, so the app still thinks they're logged in.

The RedirectUri in the authentication properties controls where users land after the Keycloak logout completes. We send them to the home page.

 

Step 7: The LoginDisplay Component

Now we need a UI element to show login state and provide login/logout actions. This typically lives in your app's header or navigation bar.

Note: NoteBookmark uses FluentUI Blazor (the <Fluent...> components), it's not a requirement, but it definitely looks great ;)

Create Components/Layout/LoginDisplay.razor:

@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        <FluentStack Orientation="Orientation.Horizontal" HorizontalGap="8" 
                     HorizontalAlignment="HorizontalAlignment.Right" 
                     VerticalAlignment="VerticalAlignment.Center">
            <span>Hello, @context.User.Identity?.Name</span>
            <FluentButton Appearance="Appearance.Lightweight" OnClick="Logout" 
                          IconStart="@(new Icons.Regular.Size16.ArrowExit())">
                Logout
            </FluentButton>
        </FluentStack>
    </Authorized>
    <NotAuthorized>
        <FluentButton Appearance="Appearance.Accent" OnClick="Login" 
                      IconStart="@(new Icons.Regular.Size16.Person())">
            Login
        </FluentButton>
    </NotAuthorized>
</AuthorizeView>

@code {
    private void Login()
    {
        var returnUrl = Navigation.ToBaseRelativePath(Navigation.Uri);
        if (string.IsNullOrEmpty(returnUrl)) returnUrl = "/";
        Navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(returnUrl)}", forceLoad: false);
    }

    private void Logout()
    {
        Navigation.NavigateTo("/logout", forceLoad: false);
    }
}

Key implementation details:

  • @rendermode InteractiveServer: This is essential. <AuthorizeView> needs to access AuthenticationStateProvider, which requires an interactive render mode. Without this, the component renders as static HTML and won't respond to auth state changes.
  • <AuthorizeView>: This component automatically shows/hides content based on authentication state. The context parameter provides access to the User claims principal.
  • Return URL on login: We pass the current page URL so users return to where they were after authenticating.
  • forceLoad: false: We use in-app navigation. The Login.razor and Logout.razor pages will handle the actual HTTP redirects.

Add this component to your MainLayout.razor or header component:<LoginDisplay />

visual of the LoginDisplay

Step 8: Protecting Routes and Pages

With login/logout working, you need to enforce authorization rules. Blazor provides two mechanisms: page-level protection with [Authorize] and inline content protection with <AuthorizeView>.

Updating Routes.razor

First, modify Components/Routes.razor to support authorization-aware routing:

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization

<FluentDesignTheme StorageName="theme" @rendermode="@InteractiveServer" />

<CascadingAuthenticationState>
    <Router AppAssembly="typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
                <NotAuthorized>
                    @if (context.User.Identity?.IsAuthenticated != true)
                    {
                        <FluentStack Orientation="Orientation.Vertical" VerticalGap="20" 
                                     HorizontalAlignment="HorizontalAlignment.Center" 
                                     Style="margin-top: 100px;">
                            <FluentIcon Value="@(new Icons.Regular.Size48.LockClosed())" Color="Color.Accent" />
                            <h2>Authentication Required</h2>
                            <p>You need to be logged in to access this page.</p>
                            <FluentButton Appearance="Appearance.Accent" 
                                OnClick="@(() => NavigationManager.NavigateTo(
                                    "/login?returnUrl=" + Uri.EscapeDataString(
                                        NavigationManager.ToBaseRelativePath(NavigationManager.Uri)), 
                                    forceLoad: false))">
                                Login
                            </FluentButton>
                        </FluentStack>
                    }
                    else
                    {
                        <FluentStack Orientation="Orientation.Vertical" VerticalGap="20" 
                                     HorizontalAlignment="HorizontalAlignment.Center" 
                                     Style="margin-top: 100px;">
                            <FluentIcon Value="@(new Icons.Regular.Size48.ShieldError())" Color="Color.Error" />
                            <h2>Access Denied</h2>
                            <p>You don't have permission to access this page.</p>
                            <FluentButton Appearance="Appearance.Accent" 
                                OnClick="@(() => NavigationManager.NavigateTo("/", forceLoad: false))">
                                Go to Home
                            </FluentButton>
                        </FluentStack>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="routeData" Selector="h1" />
        </Found>
    </Router>
</CascadingAuthenticationState>

@code {
    [Inject] private NavigationManager NavigationManager { get; set; } = default!;
}


What changed?

  1. <CascadingAuthenticationState>: This wraps the entire router and makes authentication state available to all child components. Without it, <AuthorizeView> and [Authorize] attributes won't work.

  2. <AuthorizeRouteView>: Replaces the standard RouteView. This component checks the [Authorize] attribute on routed pages and enforces authorization rules.

  3. <NotAuthorized> with two states: This is subtle but important. The <NotAuthorized> content renders when authorization fails, but there are two scenarios:

    • Not authenticated (context.User.Identity?.IsAuthenticated != true): The user isn't logged in. Show a "Login" button.
    • Authenticated but not authorized (else): The user is logged in but lacks permission (e.g., wrong role). Show an "Access Denied" message.

Protecting Pages with [Authorize]

To require authentication for an entire page, add the [Authorize] attribute:

@page "/posts"
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization

<PageTitle>My Posts</PageTitle>

<h1>My Posts</h1>

<!-- Your protected content here -->

Now, unauthenticated users who navigate to /posts will see the "Authentication Required" message from Routes.razor, not the page content.

Note: [Authorize] also supports roles and policies (e.g. [Authorize(Roles = "Admin")]) for more granular access control, that's a topic for a future post.


Testing it out:

  1. Run your Aspire app host: dotnet run --project NoteBookmark.AppHost
  2. Navigate to your Blazor app in the browser.
  3. Click "Login"—you should redirect to Keycloak, authenticate, and return.
  4. You'll see "Hello, [your name]" in the header.
  5. Navigate to a page marked [Authorize] without logging in—you'll see the auth required message.
  6. Click "Logout"—you'll sign out of both the app and Keycloak.

Your Blazor app now has full OpenID Connect authentication with Keycloak, with a clean separation between the auth mechanics (Login/Logout pages), UI (LoginDisplay), and enforcement (Routes.razor + [Authorize]).


Conclusion

You've now integrated Keycloak authentication into your .NET Aspire application. The key pieces:

  1. Aspire orchestration: AddKeycloak(), .WithReference(), and .WaitFor() handle container lifecycle and configuration injection.
  2. OIDC pipeline: The standard ASP.NET Core authentication middleware, configured for Keycloak's OIDC endpoints.
  3. HTTP flexibility: Logic to handle HTTP Keycloak in dev while enforcing HTTPS in production.
  4. Persistent data: WithDataVolume() ensures your Keycloak realm config survives restarts.

This pattern scales beyond Keycloak, Aspire's resource model works the same way for databases, message queues, and other services. Once you've mastered .WithReference() and .WaitFor(), you can compose complex distributed systems with confidence.

The full working implementation is available in the NoteBookmark repository, including the AppHost configuration, Blazor components, and Docker Compose files referenced throughout this post.


Useful links