This repository landed on exactly the local setup I wanted to try: Optimizely CMS 12, SQL Server and Keycloak in the same Aspire-orchestrated development loop, with a web application that handles both interactive login and machine-to-machine calls.
It started smaller than that. At first it was mostly a way to poke at several things I had been curious about at the same time: Aspire, Keycloak, Testcontainers and what Codex is actually good for as a project grows while you think out loud. The result was not a DXP or CMS 13 exercise, but something more grounded and, for me, more useful: getting CMS 12, the database and the identity server to start up together, importing a ready-made realm and giving a developer a reproducible way to test both auth flows locally.
While I was working on this, Avantibit Alloy + Aspire Scaffold also appeared. That post is close in spirit, but the focus here is different. There is a clearer path towards Alloy, DXP and CMS 13-like reasoning there. Here the goal is much narrower: a working local CMS 12 environment where SQL Server and Keycloak run in the same setup and where authentication can actually be tested directly.
- Aspire starts SQL Server and Keycloak together with the web application.
- Keycloak imports the realm configuration from
Aspire.OptimizelyContentDeliveryAuth.AppHost/realm-export.json. - The web application uses OIDC + cookie for interactive browser login.
- The same web application accepts bearer tokens for machine-to-machine calls.
- Two
.httpfiles show how to test the flows locally.
Aspire.OptimizelyContentDeliveryAuth.AppHostOrchestrates SQL Server, Keycloak and the web application.Aspire.OptimizelyContentDeliveryAuth.WebThe CMS 12 application.Aspire.OptimizelyContentDeliveryAuth.AppHost/realm-export.jsonThe versioned Keycloak export that makes the setup reproducible.login.httpSimple entry point for interactive login.machine-to-machine.httpFetches a token viaclient_credentialsand calls the same protected endpoint.
The important thing in the web application is that authentication is no longer an either-or choice.
- If the request arrives with
Authorization: Bearer ..., JWT bearer is used. - If the request comes from a browser without a bearer token, a cookie is used as the local session.
- If the browser is not already logged in, an OpenID Connect challenge against Keycloak is triggered.
This means https://localhost:5000/userinfo works in both cases:
- The browser is sent to Keycloak and returns with a cookie session.
- A client with an access token can call the same endpoint directly.
- .NET 10 SDK for AppHost and Aspire tooling
- .NET 8 SDK for the web project
- Docker Desktop
- HTTPS development certificate for ASP.NET Core
Run from the repository root:
dotnet run --project .\Aspire.OptimizelyContentDeliveryAuth.AppHostWhen AppHost starts, the following happens:
- SQL Server starts in a container.
- Keycloak starts in a container.
- The
optimizelyrealm is imported from the export file. - The web application starts and uses the same local environment.
- Username:
admin - Password:
abc123
This is for local development only and comes from the AppHost configuration.
The versioned realm export also includes a regular test user:
- Username:
editor - Password:
Passw0rd!
The realm export contains a confidential client for client_credentials:
- Client id:
optimizely-api - Client secret:
d4f9a2b7-c3e1-4f8a-b6d2-9e7c3a1f5b8d
Important: All credentials above are for local development only. They are hardcoded in
realm-export.json,http-client.env.jsonandAppHost.csto make it easy to get started. Never use these values in a shared, public or production-adjacent environment. Replace passwords and client secrets with strong, unique values if you expose the environment beyond your local machine.
The OIDC client for browser login is:
- Client id:
optimizely-web - Redirect URI:
https://localhost:5000/signin-oidc
Use login.http.
The file does two things:
- Starts login via the web application at
/login. - Fetches
/userinfoafter the browser session is established.
Practical flow:
- Start the solution with AppHost.
- Open
login.http. - Run the first request in browser mode.
- Log in to Keycloak with
editor/Passw0rd!. - Then run the request against
/userinfo.
You should then receive the name, authentication type and claims from the established session.
Use machine-to-machine.http together with http-client.env.json.
The file contains two requests:
- Fetch an access token from Keycloak via
client_credentials. - Call
https://localhost:5000/userinfowith the bearer token.
If your HTTP client does not automatically carry over access_token between requests, you can paste the token manually into the second request. The point here is that the entire test flow is documented and version-controlled in the .http files rather than living in loose notes.
realm-export.json is the key to making this feel like a real example rather than a demo you have to click together by hand every time.
It contains:
- the
optimizelyrealm - the browser client
optimizely-web - the machine client
optimizely-api - a demo user for interactive login
This means the setup can be cloned, started and understood without first building up Keycloak manually in the admin UI.
I wanted the auth flows to be easy to try again and easy to read. The .http files therefore became a better documentation surface than just text:
- they show exact endpoints
- they show which flow is used
- they can be re-run when the setup changes
- they serve as living notes for the next iteration
The project became a small laboratory for several ideas at once.
- Testcontainers was part of the thinking when I explored how much could be verified automatically.
- Aspire ultimately became the natural hub for the local runtime experience.
- Keycloak became the identity piece I wanted to bring into the same loop.
- Codex became the tool that helped bring together implementation, authentication setup and documentation while the project was still taking shape.
That is perhaps also the most accurate description of the repository: not a finished production pattern for everything, but a well-functioning local playground for trying several modern building blocks together.
Aspire.OptimizelyContentDeliveryAuth.AppHost/AppHost.csAspire.OptimizelyContentDeliveryAuth.AppHost/realm-export.jsonAspire.OptimizelyContentDeliveryAuth.Web/Startup.csAspire.OptimizelyContentDeliveryAuth.Web/UserInformationController.cslogin.httpmachine-to-machine.httphttp-client.env.json
Because the containers use persistent volumes, stale state may survive between runs. If you want to start completely fresh, you can remove the persistent Keycloak volume and restart AppHost.
Verify that the web application is running on https://localhost:5000 and that the optimizely-web Keycloak client still has https://localhost:5000/signin-oidc as the redirect URI.
Confirm that the token was issued by the optimizely realm and that you are using the optimizely-api client with client_credentials.
This repository does not focus on DXP, not on CMS 13, and not on covering every possible Optimizely variant. It focuses on something simpler and more concrete: getting CMS 12, SQL Server and Keycloak to live in the same local Aspire setup, with a version-controlled realm export, .http files for testing and an authentication configuration that handles both humans and machines.