diff --git a/.env-sample b/.env-sample new file mode 100644 index 0000000..3c7da79 --- /dev/null +++ b/.env-sample @@ -0,0 +1,22 @@ +# NoteBookmark Docker Compose Environment Variables +# Copy this file to .env and replace all placeholder values with your actual configuration + +# Keycloak Admin Credentials +KEYCLOAK_ADMIN_PASSWORD=your-secure-admin-password + +# Keycloak Client Configuration +KEYCLOAK_AUTHORITY=http://localhost:8080/realms/notebookmark +KEYCLOAK_CLIENT_ID=notebookmark +KEYCLOAK_CLIENT_SECRET=your-keycloak-client-secret + +# Azure Storage - Table Storage Connection +NB_STORAGE_OUTPUTS_TABLEENDPOINT=https://your-storage-account.table.core.windows.net/ + +# Azure Storage - Blob Storage Connection +NB_STORAGE_OUTPUTS_BLOBENDPOINT=https://your-storage-account.blob.core.windows.net/ + +# Notes: +# - Never commit the .env file to version control +# - Keep credentials secure and rotate them regularly +# - For local development, you can use "admin" as KEYCLOAK_ADMIN_PASSWORD +# - For production, use strong passwords and proper Azure Storage connection strings diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c030ef7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Squad: union merge for append-only team state files +.ai-team/decisions.md merge=union +.ai-team/agents/*/history.md merge=union +.ai-team/log/** merge=union +.ai-team/orchestration-log/** merge=union diff --git a/.gitignore b/.gitignore index c9650be..e56eef0 100644 --- a/.gitignore +++ b/.gitignore @@ -490,6 +490,7 @@ NoteBookmark.Api/obj/ NoteBookmark.Api/appsettings.Development.json NoteBookmark.BlazorApp/appsettings.Development.json +src/NoteBookmark.BlazorApp/appsettings.Development.json .azure NoteBookmark.AppHost/appsettings.Development.json @@ -502,4 +503,13 @@ src/NoteBookmark.AppHost/appsettings.json todos/ # AI Team folder -.ai-team/ \ No newline at end of file +.ai-team/ +.ai-team-templates/ + +# Copilot config +.copilot/ + +# Squad/Agent files +.github/agents/ +# Squad (local AI team - not committed) +.ai-team/ diff --git a/Directory.Packages.props b/Directory.Packages.props index ebf1998..0e0bb5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,51 +1,48 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index cd413dd..ca99229 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,60 @@ -# Note Bookmark - -![GitHub Release](https://img.shields.io/github/v/release/fboucher/NoteBookmark) ![.NET](https://img.shields.io/badge/9.0-512BD4?logo=dotnet&logoColor=fff) [![Unit Tests](https://github.com/FBoucher/NoteBookmark/actions/workflows/running-unit-tests.yml/badge.svg)](https://github.com/FBoucher/NoteBookmark/actions/workflows/running-unit-tests.yml) [![Reka AI](https://img.shields.io/badge/Power%20By-2E2F2F?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NjYuOTQgNjgxLjI2Ij4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmNscy0xIHsKICAgICAgICBmaWxsOiBub25lOwogICAgICB9CgogICAgICAuY2xzLTIgewogICAgICAgIGZpbGw6ICNmMWVlZTc7CiAgICAgIH0KICAgIDwvc3R5bGU%2BCiAgPC9kZWZzPgogIDxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iLS4yOSIgeT0iLS4xOSIgd2lkdGg9IjY2Ny4yMiIgaGVpZ2h0PSI2ODEuMzMiLz4KICA8Zz4KICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMxOC4zNCwwTDgyLjY3LjE2QzM2Ljg1LjE5LS4yOSwzNy4zOC0uMjksODMuMjd2MjM1LjEyaDc0LjkzVjcxLjc1aDI0My43M1YwaC0uMDNaIi8%2BCiAgICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik03Mi45NywzNjIuOTdIMHYzMTguMTZoNzIuOTd2LTMxOC4xNloiLz4KICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMxNS4zMywzNjIuODRoLTk5LjEzbC0xMDkuNSwxMDcuMjljLTEzLjk1LDEzLjY4LTIxLjgyLDMyLjM3LTIxLjgyLDUxLjkyczcuODYsMzguMjQsMjEuODIsNTEuOTJsMTA5LjUsMTA3LjI5aDEwMS42M2wtMTYyLjQ1LTE2MS43MiwxNTkuOTUtMTU2LjY3di0uMDNaIi8%2BCiAgICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zNDguNTksODIuOTJ2MTUyLjIzYzAsNDUuOTIsMzcuMTYsODMuMTEsODMuMDUsODMuMTFoMjMwLjI4di03MS43OGgtMjQwLjMzVjg1Ljg3YzAtNy43NCw2LjI4LTE0LjA2LDE0LjA1LTE0LjA2aDE0NC4zMmM3Ljc0LDAsMTQuMDUsNi4yOCwxNC4wNSwxNC4wNiwwLDUuOS0zLjcxLDExLjE3LTkuMjMsMTMuMmwtMTQ3LjQ1LDU2LjIzdjcwLjczbDE3NC41Ny02Mi4yYzMzLjA0LTExLjgsNTUuMTEtNDMuMTMsNTUuMTEtNzguMjZ2LTIuNjdDNjY3LDM3LDYyOS44LS4xOSw1ODMuOTUtLjE5aC0xNTIuMjdjLTQ1Ljg5LDAtODMuMDUsMzcuMTktODMuMDUsODMuMTFoLS4wM1oiLz4KICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTY2Ni45NCw1OTguMTJ2LTE1Mi4yM2MwLTQ1Ljg5LTM3LjE2LTgzLjExLTgzLjA1LTgzLjExaC0yMzAuMjh2NzEuNzhoMjQwLjMzdjE2MC42MWMwLDcuNzQtNi4yOCwxNC4wNi0xNC4wNSwxNC4wNmgtMTQ0LjMxYy03Ljc0LDAtMTQuMDUtNi4yOC0xNC4wNS0xNC4wNiwwLTUuOSwzLjcxLTExLjE3LDkuMjMtMTMuMmwxNDcuNDUtNTYuMjN2LTcwLjczbC0xNzQuNTcsNjIuMmMtMzMuMDQsMTEuOC01NS4xMSw0My4xMy01NS4xMSw3OC4yNnYyLjY3YzAsNDUuOTIsMzcuMTYsODMuMTEsODMuMDUsODMuMTFoMTUyLjI3YzQ1Ljg5LDAsODMuMDUtMzcuMTksODMuMDUtODMuMTFoLjAzWiIvPgogIDwvZz4KPC9zdmc%2B&logoSize=auto&labelColor=2E2F2F&color=F1EEE7)](https://reka.ai/) - - - - -I use this project mostly everyday. I build it to help me collecting my thoughts about articles, and blob posts I read during the week and then aggregate them in a #ReadingNotes blog post. You can find those post on my blog [here](https://frankysnotes.com). - -NoteBookmark is composed of three main sections: - -- **Post**: where you can manage a posts "to read", and add notes to them. -- **Generate Summary**: where you can generate a summary of the posts you read. -- **Summaries**: where you can see all the summaries you generated. - -![Slide show of all NoteBookmark Screens](gh/images/NoteBookmark-Tour_hd.gif) - -## How to deploy Your own NoteBookmark - -### Get the code on your machine - -- Fork this repository to your account. -- Clone the repository to your local machine. - - -### Deploy the solution (5 mins) - -Using Azure Developer CLI let's initialize your environment. In a terminal, at the root of the project, run the following command. When ask give it a name (ex: NoteBookmark-dev). - -```bash -azd init -``` - -Now let's deploy the solution. Run the following command in the terminal. You will have to select your Azure subscription where you want to deploy the solution, and a location (ex: eastus). - -```bash -azd up -``` - -It should take around five minutes to deploy the solution. Once it's done, you will see the URL for **Deploying service blazor-app**. - -### Secure the App in a few clicks - -The app is now deployed, but it's not secure. Navigate to the Azure Portal, and find the Resource Group you just deployed (ex: rg-notebookmark-dev). In this resource group, open the Container App **Container App**. From the left menu, select **Authentication** and click the **Add identity provider**. - -You can choose between multiple providers, I like to use Microsoft since it's deploy in Azure and I'm already logged in. If Microsoft is choose, select the recomended **Client secret expiration** (ex: 180 days). You can keep all the other default settings. Click **Add**. - -Next time you will navigate to the app, you will be prompt a to login with your Microsoft account. The first time you will have a **Permissions requested** screen, click **Accept**. - -Voila! Your app is now secure. - -## Contributing - -Your contributions are welcome! Take a look at [CONTRIBUTING](/CONTRIBUTING.md) for details. +# Note Bookmark + +![GitHub Release](https://img.shields.io/github/v/release/fboucher/NoteBookmark) ![.NET](https://img.shields.io/badge/10.0-512BD4?logo=dotnet&logoColor=fff) [![Unit Tests](https://github.com/FBoucher/NoteBookmark/actions/workflows/running-unit-tests.yml/badge.svg)](https://github.com/FBoucher/NoteBookmark/actions/workflows/running-unit-tests.yml) [![Reka AI](https://img.shields.io/badge/Power%20By-2E2F2F?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NjYuOTQgNjgxLjI2Ij4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmNscy0xIHsKICAgICAgICBmaWxsOiBub25lOwogICAgICB9CgogICAgICAuY2xzLTIgewogICAgICAgIGZpbGw6ICNmMWVlZTc7CiAgICAgIH0KICAgIDwvc3R5bGU%2BCiAgPC9kZWZzPgogIDxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iLS4yOSIgeT0iLS4xOSIgd2lkdGg9IjY2Ny4yMiIgaGVpZ2h0PSI2ODEuMzMiLz4KICA8Zz4KICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMxOC4zNCwwTDgyLjY3LjE2QzM2Ljg1LjE5LS4yOSwzNy4zOC0uMjksODMuMjd2MjM1LjEyaDc0LjkzVjcxLjc1aDI0My43M1YwaC0uMDNaIi8%2BCiAgICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik03Mi45NywzNjIuOTdIMHYzMTguMTZoNzIuOTd2LTMxOC4xNloiLz4KICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMxNS4zMywzNjIuODRoLTk5LjEzbC0xMDkuNSwxMDcuMjljLTEzLjk1LDEzLjY4LTIxLjgyLDMyLjM3LTIxLjgyLDUxLjkyczcuODYsMzguMjQsMjEuODIsNTEuOTJsMTA5LjUsMTA3LjI5aDEwMS42M2wtMTYyLjQ1LTE2MS43MiwxNTkuOTUtMTU2LjY3di0uMDNaIi8%2BCiAgICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zNDguNTksODIuOTJ2MTUyLjIzYzAsNDUuOTIsMzcuMTYsODMuMTEsODMuMDUsODMuMTFoMjMwLjI4di03MS43OGgtMjQwLjMzVjg1Ljg3YzAtNy43NCw2LjI4LTE0LjA2LDE0LjA1LTE0LjA2aDE0NC4zMmM3Ljc0LDAsMTQuMDUsNi4yOCwxNC4wNSwxNC4wNiwwLDUuOS0zLjcxLDExLjE3LTkuMjMsMTMuMmwtMTQ3LjQ1LDU2LjIzdjcwLjczbDE3NC41Ny02Mi4yYzMzLjA0LTExLjgsNTUuMTEtNDMuMTMsNTUuMTEtNzguMjZ2LTIuNjdDNjY3LDM3LDYyOS44LS4xOSw1ODMuOTUtLjE5aC0xNTIuMjdjLTQ1Ljg5LDAtODMuMDUsMzcuMTktODMuMDUsODMuMTFoLS4wM1oiLz4KICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTY2Ni45NCw1OTguMTJ2LTE1Mi4yM2MwLTQ1Ljg5LTM3LjE2LTgzLjExLTgzLjA1LTgzLjExaC0yMzAuMjh2NzEuNzhoMjQwLjMzdjE2MC42MWMwLDcuNzQtNi4yOCwxNC4wNi0xNC4wNSwxNC4wNmgtMTQ0LjMxYy03Ljc0LDAtMTQuMDUtNi4yOC0xNC4wNS0xNC4wNiwwLTUuOSwzLjcxLTExLjE3LDkuMjMtMTMuMmwxNDcuNDUtNTYuMjN2LTcwLjczbC0xNzQuNTcsNjIuMmMtMzMuMDQsMTEuOC01NS4xMSw0My4xMy01NS4xMSw3OC4yNnYyLjY3YzAsNDUuOTIsMzcuMTYsODMuMTEsODMuMDUsODMuMTFoMTUyLjI3YzQ1Ljg5LDAsODMuMDUtMzcuMTksODMuMDUtODMuMTFoLjAzWiIvPgogIDwvZz4KPC9zdmc%2B&logoSize=auto&labelColor=2E2F2F&color=F1EEE7)](https://reka.ai/) + + + + +I use this project mostly everyday. I build it to help me collecting my thoughts about articles, and blob posts I read during the week and then aggregate them in a #ReadingNotes blog post. You can find those post on my blog [here](https://frankysnotes.com). + +NoteBookmark is composed of three main sections: + +- **Post**: where you can manage a posts "to read", and add notes to them. +- **Generate Summary**: where you can generate a summary of the posts you read. +- **Summaries**: where you can see all the summaries you generated. + +![Slide show of all NoteBookmark Screens](gh/images/NoteBookmark-Tour_hd.gif) + +## How to deploy Your own NoteBookmark + +### Get the code on your machine + +- Fork this repository to your account. +- Clone the repository to your local machine. + + +### Deploy the solution (5 mins) + +Using Azure Developer CLI let's initialize your environment. In a terminal, at the root of the project, run the following command. When ask give it a name (ex: NoteBookmark-dev). + +```bash +azd init +``` + +Now let's deploy the solution. Run the following command in the terminal. You will have to select your Azure subscription where you want to deploy the solution, and a location (ex: eastus). + +```bash +azd up +``` + +It should take around five minutes to deploy the solution. Once it's done, you will see the URL for **Deploying service blazor-app**. + +### Secure the App in a few clicks + +The app is now deployed, but it's not secure. Navigate to the Azure Portal, and find the Resource Group you just deployed (ex: rg-notebookmark-dev). In this resource group, open the Container App **Container App**. From the left menu, select **Authentication** and click the **Add identity provider**. + +You can choose between multiple providers, I like to use Microsoft since it's deploy in Azure and I'm already logged in. If Microsoft is choose, select the recomended **Client secret expiration** (ex: 180 days). You can keep all the other default settings. Click **Add**. + +Next time you will navigate to the app, you will be prompt a to login with your Microsoft account. The first time you will have a **Permissions requested** screen, click **Accept**. + +Voila! Your app is now secure. + +## Documentation + +For detailed setup guides and configuration information: +- [Keycloak Authentication Setup](/docs/keycloak-setup.md) - Complete guide for setting up Keycloak authentication +- [Docker Compose Deployment](/docs/docker-compose-deployment.md) - Deploy with Docker Compose (generate from Aspire or use provided files) + +## Contributing + +Your contributions are welcome! Take a look at [CONTRIBUTING](/CONTRIBUTING.md) for details. diff --git a/docker-compose/docker-compose.yaml b/docker-compose/docker-compose.yaml index 01fb91d..3afe790 100644 --- a/docker-compose/docker-compose.yaml +++ b/docker-compose/docker-compose.yaml @@ -1,6 +1,23 @@ name: note-bookmark services: + keycloak: + image: "quay.io/keycloak/keycloak:26.1" + container_name: "notebookmark-keycloak" + command: ["start-dev"] + environment: + KEYCLOAK_ADMIN: "admin" + KEYCLOAK_ADMIN_PASSWORD: "${KEYCLOAK_ADMIN_PASSWORD:-admin}" + KC_HTTP_PORT: "8080" + KC_HOSTNAME_STRICT: "false" + KC_HOSTNAME_STRICT_HTTPS: "false" + KC_HTTP_ENABLED: "true" + ports: + - "8080:8080" + volumes: + - keycloak-data:/opt/keycloak/data + networks: + - "aspire" api: image: "fboucher/notebookmark-api:latest" container_name: "notebookmark-api" @@ -27,14 +44,23 @@ services: ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true" HTTP_PORTS: "8004" services__api__http__0: "http://api:8000" + services__keycloak__http__0: "http://keycloak:8080" + Keycloak__Authority: "${KEYCLOAK_AUTHORITY:-http://keycloak:8080/realms/notebookmark}" + Keycloak__ClientId: "${KEYCLOAK_CLIENT_ID:-notebookmark}" + Keycloak__ClientSecret: "${KEYCLOAK_CLIENT_SECRET}" ports: - "8005:8004" - "8007:8006" depends_on: api: condition: "service_started" + keycloak: + condition: "service_started" networks: - "aspire" +volumes: + keycloak-data: + driver: "local" networks: aspire: driver: "bridge" diff --git a/docs/docker-compose-deployment.md b/docs/docker-compose-deployment.md new file mode 100644 index 0000000..acdf9e2 --- /dev/null +++ b/docs/docker-compose-deployment.md @@ -0,0 +1,177 @@ +# Docker Compose Deployment + +This guide explains how to deploy NoteBookmark using Docker Compose, either by generating it fresh from Aspire or using the provided compose file. + +## Two Deployment Options + +### Option 1: Generate from Aspire (Recommended) + +Generate an up-to-date docker-compose.yaml from your Aspire AppHost configuration using the official Aspire CLI. + +**Prerequisites:** +- .NET Aspire Workload installed: `dotnet workload install aspire` +- Aspire CLI installed: Included with the Aspire workload + +**Steps:** + +1. **Build container images locally:** + + The generated docker-compose file references image names (e.g., `notebookmark-api`, `notebookmark-blazor`), but these images don't exist until you build them. Build and tag the images with the expected names: + + ```bash + # Build API image + dotnet publish src/NoteBookmark.Api/NoteBookmark.Api.csproj -c Release -t:PublishContainer + + # Build Blazor app image + dotnet publish src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj -c Release -t:PublishContainer + ``` + + These commands build the projects and create Docker images tagged as `notebookmark-api:latest` and `notebookmark-blazorapp:latest` (based on your project names). The container names `notebookmark-api` and `notebookmark-blazor` are what the running containers will be called. + +2. **Publish the application (generates Docker Compose files):** + ```bash + aspire publish --output-path ./aspire-output --project-name notebookmark + ``` + + **Parameters:** + - `--output-path`: Directory where docker-compose files will be generated (default: `aspire-output`) + - `--project-name`: Docker Compose project name (sets `name:` at the top of docker-compose.yaml) + - Without this, the project name defaults to the output directory name + - Affects container names: `notebookmark-api`, `notebookmark-blazor` vs `aspire-output-api`, `aspire-output-blazor` + + This command generates: + - `docker-compose.yaml` from the AppHost configuration + - `.env` file template with expected parameters (unfilled) + - Supporting infrastructure files (Bicep, Azure configs if applicable) + +3. **Fill in environment variables:** + Edit `./aspire-output/.env` and replace placeholder values with your actual configuration: + - Azure Storage connection strings + - Keycloak admin password and client secrets + - Any other environment-specific settings + +4. **Deploy (optional - full workflow):** + ```bash + aspire deploy --output-path ./aspire-output + ``` + This performs the complete workflow: publishes, prepares environment configs, builds images, and runs `docker compose up`. + + Or manually run Docker Compose from the output directory: + ```bash + cd aspire-output + docker compose up -d + ``` + +This ensures your docker-compose file stays in sync with the latest AppHost configuration. + +> **📚 Learn more:** See the [official Aspire Docker integration documentation](https://aspire.dev/integrations/compute/docker/) for advanced scenarios like environment-specific configs and custom image tagging. + +### Option 2: Use the Provided Compose File (Quick Start) + +For a quick start, you can use the checked-in docker-compose.yaml file located in the `docker-compose/` directory. This file was generated from Aspire and committed to the repository for convenience. + +**When to use this option:** +- You want to quickly test the application without regenerating compose files +- You're deploying a stable release version +- You haven't modified the AppHost configuration + +**Important:** If you've modified `src/NoteBookmark.AppHost/AppHost.cs`, use Option 1 to regenerate the compose file to reflect your changes. + +## Environment Configuration + +The docker-compose.yaml file uses environment variables for configuration. You must create a `.env` file in the same directory as your docker-compose.yaml file. + +### What the .env File Is For + +The `.env` file contains sensitive configuration values needed for production deployment: + +- **Database connection strings**: Connection to Azure Table Storage and Blob Storage +- **Keycloak configuration**: Authentication server settings (authority URL, client credentials) +- **Other runtime settings**: Any environment-specific configurations + +### Creating Your .env File + +1. Copy the `.env-sample` file from the repository root: + ```bash + cp .env-sample .env + ``` + +2. Edit `.env` and replace all placeholder values with your actual configuration: + - Azure Storage connection strings + - Keycloak admin password + - Keycloak client secret + - Keycloak authority URL (if different from default) + +3. Keep `.env` secure and never commit it to version control (it's in .gitignore) + +## Running the Application + +Once your `.env` file is configured: + +**If using Option 1 (Aspire-generated):** +```bash +cd aspire-output +docker compose up -d +``` + +**If using Option 2 (checked-in file):** +```bash +cd docker-compose +docker compose up -d +``` + +Access the application at: +- **Blazor App**: http://localhost:8005 +- **API**: http://localhost:8001 +- **Keycloak**: http://localhost:8080 + +**First-time setup:** Keycloak needs to be configured with the realm settings. See [Keycloak Setup Guide](./keycloak-setup.md) for detailed instructions. + +## Stopping the Application + +```bash +docker compose down +``` + +To also remove volumes (WARNING: This deletes Keycloak data): + +```bash +docker compose down -v +``` + +## Advanced Deployment Workflows + +The Aspire CLI supports environment-specific deployments: + +**Prepare for a specific environment:** +```bash +# For staging +aspire do prepare-docker-env --environment staging + +# For production +aspire do prepare-docker-env --environment production +``` + +This generates environment-specific `.env` files and builds container images. + +**Clean up a deployment:** +```bash +aspire do docker-compose-down-docker-env +``` + +This stops and removes all containers, networks, and volumes. + +## Notes + +- **Development vs Production:** + - In development (`dotnet run`), Aspire manages Keycloak automatically via `AddKeycloak()` + - In production (docker-compose), Keycloak runs as a containerized service + - The AppHost uses `AddDockerComposeEnvironment("docker-env")` to signal Azure Container Apps deployment intent + +- **Service Discovery:** + - Development: Aspire service discovery wires up connections automatically + - Production: Services connect via explicit environment variables in `.env` + +- **Data Persistence:** + - Keycloak data persists in a named volume (`keycloak-data`) + - Use `docker compose down -v` carefully — it deletes all data including Keycloak configuration diff --git a/docs/keycloak-setup.md b/docs/keycloak-setup.md new file mode 100644 index 0000000..f62248c --- /dev/null +++ b/docs/keycloak-setup.md @@ -0,0 +1,82 @@ +# Keycloak Authentication Setup + +## Overview + +NoteBookmark now requires authentication via Keycloak (or any OpenID Connect provider). Only the home page is accessible without authentication - all other pages require a logged-in user. + +## Configuration + +### 1. Keycloak Server Setup + +You'll need a Keycloak server running. For local development: + +```bash +docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev +``` + +### 2. Create a Realm + +1. Log into Keycloak admin console (http://localhost:8080) +2. Create a new realm called "notebookmark" + +### 3. Create a Client + +1. In the realm, create a new client: + - Client ID: `notebookmark` + - Client Protocol: `openid-connect` + - Access Type: `confidential` + - Valid Redirect URIs: `https://localhost:5001/*` (adjust for your environment) + - Web Origins: `https://localhost:5001` (adjust for your environment) + +2. Get the client secret from the Credentials tab + +### 4. Configure the Application + +Update `appsettings.json` or environment variables: + +```json +{ + "Keycloak": { + "Authority": "http://localhost:8080/realms/notebookmark", + "ClientId": "notebookmark", + "ClientSecret": "your-client-secret-here" + } +} +``` + +**Environment Variables (recommended for production):** + +```bash +export Keycloak__Authority="https://your-keycloak-server/realms/notebookmark" +export Keycloak__ClientId="notebookmark" +export Keycloak__ClientSecret="your-secret" +``` + +### 5. Add Users + +In Keycloak, create users in the realm who should have access to your private website. + +## How It Works + +- **Home page (/)**: Public - no authentication required +- **All other pages**: Protected with `[Authorize]` attribute +- **Login/Logout**: UI in the header shows login button when not authenticated +- **Session**: Uses cookie-based authentication with OpenID Connect + +## Technical Details + +- Uses `Microsoft.AspNetCore.Authentication.OpenIdConnect` package +- Cookie-based session management +- Authorization state cascaded throughout the component tree +- `AuthorizeRouteView` in Routes.razor handles route-level protection + +## Files Modified + +- `Program.cs`: Added authentication middleware and configuration +- `Routes.razor`: Changed to `AuthorizeRouteView` for authorization support +- `MainLayout.razor`: Added `LoginDisplay` component to header +- `_Imports.razor`: Added authorization namespaces +- All pages except `Home.razor`: Added `@attribute [Authorize]` +- `Components/Shared/LoginDisplay.razor`: New component for login/logout UI +- `Components/Pages/Login.razor`: Login page +- `Components/Pages/Logout.razor`: Logout page diff --git a/src/NoteBookmark.AppHost/AppHost.cs b/src/NoteBookmark.AppHost/AppHost.cs index d9460d7..264e783 100644 --- a/src/NoteBookmark.AppHost/AppHost.cs +++ b/src/NoteBookmark.AppHost/AppHost.cs @@ -1,37 +1,83 @@ +using Aspire.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Projects; var builder = DistributedApplication.CreateBuilder(args); -#pragma warning disable ASPIRECOMPUTE001 +// Load docker-compose environment var compose = builder.AddDockerComposeEnvironment("docker-env"); -var noteStorage = builder.AddAzureStorage("nb-storage"); - -var apiKey = builder.Configuration["AppSettings:REKA_API_KEY"] ?? Environment.GetEnvironmentVariable("REKA_API_KEY") ?? throw new InvalidOperationException("REKA_API_KEY environment variable is not set."); +// Add Keycloak authentication server +var keycloak = builder.AddKeycloak("keycloak", port: 8080) + .WithDataVolume(); // Persist Keycloak data across container restarts if (builder.Environment.IsDevelopment()) { + + var noteStorage = builder.AddAzureStorage("nb-storage"); + + var apiKey = builder.Configuration["AppSettings:REKA_API_KEY"] ?? Environment.GetEnvironmentVariable("REKA_API_KEY") ?? throw new InvalidOperationException("REKA_API_KEY environment variable is not set."); + noteStorage.RunAsEmulator(); + + var tables = noteStorage.AddTables("nb-tables"); + var blobs = noteStorage.AddBlobs("nb-blobs"); + + var api = builder.AddProject("api") + .WithReference(tables) + .WithReference(blobs) + .WaitFor(tables) + .WaitFor(blobs) + .PublishAsDockerComposeService((resource, service) => + { + service.ContainerName = "notebookmark-api"; + }); + + builder.AddProject("blazor-app") + .WithReference(api) + .WithReference(tables) // Server-side access to Azure Tables for unmasked settings + .WithReference(keycloak) // Reference Keycloak for authentication + .WaitFor(api) + .WaitFor(keycloak) + //.WaitFor(compose) // Wait for docker-compose services to be ready + .WithExternalHttpEndpoints() + .WithEnvironment("REKA_API_KEY", apiKey) + .PublishAsDockerComposeService((resource, service) => + { + service.ContainerName = "notebookmark-blazor"; + }); } +else +{ + // Production mode - no Aspire resources, expects docker-compose or Azure deployment + var noteStorage = builder.AddAzureStorage("nb-storage"); + + var apiKey = builder.Configuration["AppSettings:REKA_API_KEY"] ?? Environment.GetEnvironmentVariable("REKA_API_KEY") ?? throw new InvalidOperationException("REKA_API_KEY environment variable is not set."); -var tables = noteStorage.AddTables("nb-tables"); -var blobs = noteStorage.AddBlobs("nb-blobs"); - -var api = builder.AddProject("api") - .WithReference(tables) - .WithReference(blobs) - .WaitFor(tables) - .WaitFor(blobs) - .WithComputeEnvironment(compose); // comment this line to deploy to Azure - -builder.AddProject("blazor-app") - .WithReference(api) - .WithReference(tables) // Server-side access to Azure Tables for unmasked settings - .WaitFor(api) - .WithExternalHttpEndpoints() - .WithEnvironment("REKA_API_KEY", apiKey) - .WithComputeEnvironment(compose); // comment this line to deploy to Azure + var tables = noteStorage.AddTables("nb-tables"); + var blobs = noteStorage.AddBlobs("nb-blobs"); + + var api = builder.AddProject("api") + .WithReference(tables) + .WithReference(blobs) + .WaitFor(tables) + .WaitFor(blobs) + .PublishAsDockerComposeService((resource, service) => + { + service.ContainerName = "notebookmark-api"; + }); + + builder.AddProject("blazor-app") + .WithReference(api) + .WithReference(tables) // Server-side access to Azure Tables for unmasked settings + .WaitFor(api) + .WithExternalHttpEndpoints() + .WithEnvironment("REKA_API_KEY", apiKey) + .PublishAsDockerComposeService((resource, service) => + { + service.ContainerName = "notebookmark-blazor"; + }); +} builder.Build().Run(); diff --git a/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj b/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj index 2267fb5..3e6cdf8 100644 --- a/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj +++ b/src/NoteBookmark.AppHost/NoteBookmark.AppHost.csproj @@ -1,17 +1,17 @@ - - - - Exe - true - 0784f0a9-b1e6-4e65-8d31-00f1369f6d75 - - - - - - - - - - + + + Exe + true + 0784f0a9-b1e6-4e65-8d31-00f1369f6d75 + + + + + + + + + + + \ No newline at end of file diff --git a/src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor b/src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor index 340eae8..097843c 100644 --- a/src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor +++ b/src/NoteBookmark.BlazorApp/Components/Layout/MainLayout.razor @@ -1,45 +1,49 @@ -@inherits LayoutComponentBase -@using Microsoft.FluentUI.AspNetCore.Components -@using Microsoft.FluentUI.AspNetCore.Components.Extensions -@inject NavigationManager NavigationManager - - - - Note Bookmark - - - - - - - - -
- @Body -
-
-
- - Documentation and demos - - About Blazor - -
- - - - - -
- An unhandled error has occurred. - Reload - 🗙 -
- - - -@code { - - public DesignThemeModes Mode { get; set; } - public OfficeColor? OfficeColor { get; set; } -} +@inherits LayoutComponentBase +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@using NoteBookmark.BlazorApp.Components.Shared +@inject NavigationManager NavigationManager + + + + Note Bookmark + + + + + + + + + + + +
+ @Body +
+
+
+ + Documentation and demos + + About Blazor + +
+ + + + + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + +@code { + + public DesignThemeModes Mode { get; set; } + public OfficeColor? OfficeColor { get; set; } +} diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Error.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Error.razor index 576cc2d..cd14c6e 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Error.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Error.razor @@ -1,4 +1,6 @@ @page "/Error" +@attribute [AllowAnonymous] +@using Microsoft.AspNetCore.Authorization @using System.Diagnostics Error diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor index 6f7e2e6..c00d62b 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Home.razor @@ -1,66 +1,68 @@ -@page "/" -@using Microsoft.FluentUI.AspNetCore.Components -@inject NavigationManager Navigation - -Home - NoteBookmark - - -

📚 NoteBookmark

- - -

Your personal reading companion for capturing thoughts and insights from articles and blog posts. - Transform your reading notes into polished summaries, perfect for sharing your weekly discoveries.

-
- - - - -
📝
-

Manage Posts

-

Collect articles to read and add your notes as you go through them.

-
-
- - - -
🔍
-

AI-Powered Search

-

Discover relevant content with intelligent suggestions tailored to your interests.

-
-
- - - -
-

Generate Summaries

-

Create beautiful summaries of your reading notes with AI assistance.

-
-
-
- - - - -

Built with Modern Tech

- - - .NET 9 - - - Blazor - - - Fluent UI Blazor - - - Aspire - - - Azure Table Storage - - - Reka AI - - -
+@page "/" +@attribute [AllowAnonymous] +@using Microsoft.AspNetCore.Authorization +@using Microsoft.FluentUI.AspNetCore.Components +@inject NavigationManager Navigation + +Home - NoteBookmark + + +

📚 NoteBookmark

+ + +

Your personal reading companion for capturing thoughts and insights from articles and blog posts. + Transform your reading notes into polished summaries, perfect for sharing your weekly discoveries.

+
+ + + + +
📝
+

Manage Posts

+

Collect articles to read and add your notes as you go through them.

+
+
+ + + +
🔍
+

AI-Powered Search

+

Discover relevant content with intelligent suggestions tailored to your interests.

+
+
+ + + +
+

Generate Summaries

+

Create beautiful summaries of your reading notes with AI assistance.

+
+
+
+ + + + +

Built with Modern Tech

+ + + .NET 9 + + + Blazor + + + Fluent UI Blazor + + + Aspire + + + Azure Table Storage + + + Reka AI + + +
\ No newline at end of file diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Login.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Login.razor new file mode 100644 index 0000000..bb64738 --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Login.razor @@ -0,0 +1,27 @@ +@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() + { + // Get the return URL from query string or default to home + var uri = new Uri(Navigation.Uri); + var query = System.Web.HttpUtility.ParseQueryString(uri.Query); + var returnUrl = query["returnUrl"] ?? "/"; + + // Trigger authentication challenge via HttpContext + var httpContext = HttpContextAccessor.HttpContext; + if (httpContext != null) + { + var authProperties = new AuthenticationProperties + { + RedirectUri = returnUrl + }; + await httpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties); + } + } +} diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Logout.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Logout.razor new file mode 100644 index 0000000..4bfd9da --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Logout.razor @@ -0,0 +1,22 @@ +@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); + } + } +} diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor b/src/NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor index 0167b46..d2d5f52 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/PostEditor.razor @@ -1,5 +1,6 @@ @page "/posteditor/{id?}" - +@attribute [Authorize] +@using Microsoft.AspNetCore.Authorization @using NoteBookmark.BlazorApp @using NoteBookmark.Domain @inject PostNoteClient client diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor b/src/NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor index 8448b02..2f7a149 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/PostEditorLight.razor @@ -1,5 +1,6 @@ @page "/posteditorlight/{id?}" - +@attribute [Authorize] +@using Microsoft.AspNetCore.Authorization @using NoteBookmark.BlazorApp @using NoteBookmark.BlazorApp.Components.Layout @using NoteBookmark.Domain diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor index 1d2672d..fdfef51 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Posts.razor @@ -1,4 +1,6 @@ @page "/posts" +@attribute [Authorize] +@using Microsoft.AspNetCore.Authorization @using NoteBookmark.BlazorApp.Components.Shared @using NoteBookmark.Domain @using Microsoft.FluentUI.AspNetCore.Components diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor index 9c4b02d..4eab507 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Search.razor @@ -1,4 +1,6 @@ @page "/search" +@attribute [Authorize] +@using Microsoft.AspNetCore.Authorization @using NoteBookmark.AIServices @using NoteBookmark.BlazorApp.Components.Shared @using NoteBookmark.Domain diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor index 4b3a16d..bc250c9 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Settings.razor @@ -1,5 +1,6 @@ @page "/settings" - +@attribute [Authorize] +@using Microsoft.AspNetCore.Authorization @using Microsoft.FluentUI.AspNetCore.Components.Extensions @using NoteBookmark.Domain @inject ILogger Logger diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/Summaries.razor b/src/NoteBookmark.BlazorApp/Components/Pages/Summaries.razor index c9dc2fb..9009a4f 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/Summaries.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/Summaries.razor @@ -1,4 +1,6 @@ @page "/summaries" +@attribute [Authorize] +@using Microsoft.AspNetCore.Authorization @using NoteBookmark.Domain @inject PostNoteClient client @rendermode InteractiveServer diff --git a/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor b/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor index c9bfd49..4397287 100644 --- a/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor +++ b/src/NoteBookmark.BlazorApp/Components/Pages/SummaryEditor.razor @@ -1,5 +1,6 @@ @page "/summaryeditor/{number?}" - +@attribute [Authorize] +@using Microsoft.AspNetCore.Authorization @using Markdig @using NoteBookmark.Domain @using NoteBookmark.AIServices diff --git a/src/NoteBookmark.BlazorApp/Components/Routes.razor b/src/NoteBookmark.BlazorApp/Components/Routes.razor index 4d3379c..842b358 100644 --- a/src/NoteBookmark.BlazorApp/Components/Routes.razor +++ b/src/NoteBookmark.BlazorApp/Components/Routes.razor @@ -1,8 +1,42 @@ - +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Authorization - - - - - - + + + + + + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + +

Authentication Required

+

You need to be logged in to access this page.

+ + Login + +
+ } + else + { + + +

Access Denied

+

You don't have permission to access this page.

+ + Go to Home + +
+ } +
+
+ +
+
+
+ +@code { + [Inject] private NavigationManager NavigationManager { get; set; } = default!; +} diff --git a/src/NoteBookmark.BlazorApp/Components/Shared/LoginDisplay.razor b/src/NoteBookmark.BlazorApp/Components/Shared/LoginDisplay.razor new file mode 100644 index 0000000..d3b4595 --- /dev/null +++ b/src/NoteBookmark.BlazorApp/Components/Shared/LoginDisplay.razor @@ -0,0 +1,36 @@ +@rendermode InteractiveServer +@using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager Navigation + + + + + Hello, @context.User.Identity?.Name + + Logout + + + + + + Login + + + + +@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); + } +} diff --git a/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj index 7cf93ce..bc0fb7c 100644 --- a/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj +++ b/src/NoteBookmark.BlazorApp/NoteBookmark.BlazorApp.csproj @@ -4,6 +4,7 @@ + diff --git a/src/NoteBookmark.BlazorApp/Program.cs b/src/NoteBookmark.BlazorApp/Program.cs index bdee7e0..d169a42 100644 --- a/src/NoteBookmark.BlazorApp/Program.cs +++ b/src/NoteBookmark.BlazorApp/Program.cs @@ -1,3 +1,6 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.FluentUI.AspNetCore.Components; using NoteBookmark.AIServices; using NoteBookmark.BlazorApp; @@ -57,6 +60,78 @@ }); +// 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 for development/docker scenarios. + // If not explicitly configured, relax the requirement when running in a container against HTTP Keycloak. + var requireHttpsConfigured = builder.Configuration.GetValue("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 properly pass id_token_hint to Keycloak + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProviderForSignOut = async context => + { + // Get the id_token from saved tokens + 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(); + // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); @@ -78,7 +153,30 @@ app.UseStaticFiles(); app.UseAntiforgery(); +app.UseAuthentication(); +app.UseAuthorization(); + app.MapRazorComponents() .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();