Skip to content

Commit f4ebd9f

Browse files
Merge pull request #14 from workcontrolgit/develop
Align series 5 articles with Key Vault strategy
2 parents 3658b07 + d552504 commit f4ebd9f

3 files changed

Lines changed: 79 additions & 37 deletions

File tree

blogs/series-5-devops-data/5.5-azure-oidc-github-actions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ The output should include a row with `Contributor` assigned to `github-actions-t
300300

301301
**`SQL_ADMIN_PASSWORD` added manually.** The SQL admin password is a genuine secret — a value that must remain confidential. The three OIDC values (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`) are not sensitive in the same way: knowing them without the federated credential is harmless. The SQL password is kept separate and typed interactively, never written to a file or echoed in a terminal.
302302

303+
**GitHub Secrets vs Azure Key Vault — the clear split.** `SQL_ADMIN_PASSWORD` is the only runtime-sensitive value in GitHub Secrets for this project — and it is only used once, during the Bicep infrastructure deployment (Article 5.4). It is not used by the application itself. Database connection strings, JWT signing keys, and other **application runtime secrets** are stored in Azure Key Vault (`kv-talent-dev`), not in GitHub Secrets. The deploy workflows in Articles 5.6 and 5.7 inject `@Microsoft.KeyVault(SecretUri=...)` references into App Service settings rather than raw values. The Web App's managed identity resolves those references at runtime — the actual secret never passes through GitHub Actions at all.
304+
303305
---
304306

305307
## 🌟 Why This Matters

blogs/series-5-devops-data/5.6-azure-deploy-dotnet-apps.md

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ This article is part of the **AngularNetTutorial** series. The full-stack tutori
2323
* **`dotnet publish`** — what the publish output contains and why it differs from the build output
2424
* **`azure/webapps-deploy`** — the action that uploads and hot-swaps the deployment
2525
* **App Service settings** — how connection strings and URLs reach the running app without touching `appsettings.json`
26+
* **Key Vault references** — why connection strings go into Key Vault, not GitHub Secrets, and how `@Microsoft.KeyVault(SecretUri=...)` wires them into App Service settings
27+
* **GitHub Secrets vs Key Vault** — the split: CI/CD auth credentials in GitHub Secrets, runtime secrets in Key Vault
2628
* **Deployment order** — why IdentityServer must be deployed and running before the API
2729

2830
---
@@ -41,6 +43,8 @@ This article is part of the **AngularNetTutorial** series. The full-stack tutori
4143

4244
The .NET API and IdentityServer each need several environment-specific values at runtime: the database connection string, the URL of IdentityServer (which the API uses to validate tokens), and the URL of the API itself (which IdentityServer uses to register the audience). These values differ between local development and Azure. Hardcoding them in `appsettings.json` would commit environment-specific secrets to source control and break the principle that the same build artifact runs in every environment.
4345

46+
Even storing connection strings in GitHub Secrets is not production-grade — the raw value gets injected into App Service settings where it is visible in plain text in the Portal to anyone with Contributor access. The correct approach is to store secrets in **Azure Key Vault** (provisioned in Article 5.4) and reference them from App Service settings via `@Microsoft.KeyVault(SecretUri=...)`. The App Service resolves the reference at runtime using its managed identity — the actual connection string is never visible anywhere.
47+
4448
Manual deployment — `dotnet publish`, zip the output, upload via the Portal — is error-prone and produces no audit trail. Running it inconsistently across developers produces inconsistent results.
4549

4650
---
@@ -51,28 +55,33 @@ GitHub Actions workflows are triggered by pushes to specific paths. When code ch
5155

5256
App Service application settings are the Azure equivalent of environment variables. They override values in `appsettings.json` at runtime without touching the committed file. The same published binary runs locally (using `appsettings.json`) and in Azure (using App Service settings that override the file).
5357

58+
For **non-sensitive settings** (URLs, feature flags, audience names), the workflow injects plain values directly. For **secrets** (connection strings, JWT signing key), the workflow injects a `@Microsoft.KeyVault(SecretUri=...)` reference instead of the raw value. App Service resolves these references at startup using the Web App's system-assigned managed identity — the actual secret is retrieved from Key Vault and injected into the app's environment without ever appearing in the Portal or in logs. See Article 5.9 for full detail on this pattern.
59+
5460
---
5561

5662
## 🚀 How It Works
5763

5864
### Step 1: Add the Remaining GitHub Secrets
5965

60-
Articles 5.5 set up four secrets. Two workflows need several more:
66+
Article 5.5 set up four secrets (`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`, `SQL_ADMIN_PASSWORD`). The deploy workflows need several more.
6167

6268
**Navigate to:** GitHub → Repository → Settings → Secrets and variables → Actions → New repository secret
6369

64-
**Secrets to add:**
70+
**GitHub Secrets vs Azure Key Vault — the split:**
6571

66-
* **`API_DB_CONNECTION_STRING`** — the Azure SQL connection string for the API database
72+
| Secret type | Where it lives | Why |
73+
|---|---|---|
74+
| CI/CD auth credentials (`AZURE_*`) | GitHub Secrets | Needed by the runner to log in to Azure |
75+
| Infrastructure deploy password (`SQL_ADMIN_PASSWORD`) | GitHub Secrets | One-time use for Bicep provisioning only |
76+
| Runtime secrets (connection strings, JWT key) | **Azure Key Vault** | Never exposed as plain text; resolved by managed identity at runtime |
77+
| Non-sensitive URLs and config | GitHub Secrets | Not sensitive — just convenient to store here |
6778

68-
```
69-
Server=tcp:sql-talent-dev.database.windows.net,1433;Initial Catalog=sqldb-talent-api-dev;Persist Security Info=False;User ID=sqladmin;Password=<your-password>;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
70-
```
79+
**Secrets to add to GitHub:**
7180

72-
* **`IDS_DB_CONNECTION_STRING`** — the Azure SQL connection string for the IdentityServer database
81+
* **`KEY_VAULT_URI`** — the URI of the Key Vault provisioned in Article 5.4
7382

7483
```
75-
Server=tcp:sql-talent-dev.database.windows.net,1433;Initial Catalog=sqldb-talent-ids-dev;Persist Security Info=False;User ID=sqladmin;Password=<your-password>;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
84+
https://kv-talent-dev.vault.azure.net/
7685
```
7786

7887
* **`IDENTITY_SERVER_URL`** — the HTTPS URL of the deployed IdentityServer App Service
@@ -81,20 +90,41 @@ Server=tcp:sql-talent-dev.database.windows.net,1433;Initial Catalog=sqldb-talent
8190
https://app-talent-ids-dev.azurewebsites.net
8291
```
8392

84-
* **`JWT_KEY`** — the symmetric key used by the API's local JWT authentication (copy from `appsettings.json``JWTSettings.Key`)
85-
* **`ANGULAR_APP_URL`** — the Azure Static Web App URL (e.g. `https://mango-flower-0ced4011e.4.azurestaticapps.net`) — added to API CORS allowed origins
86-
* **`IDENTITY_ADMIN_URL`** — the IdentityServer Admin UI URL (e.g. `https://app-talent-admin-dev.azurewebsites.net`) — used by STS and Admin app configuration
93+
* **`ANGULAR_APP_URL`** — the Azure Static Web App URL
94+
95+
```
96+
https://mango-flower-0ced4011e.4.azurestaticapps.net
97+
```
98+
99+
* **`IDENTITY_ADMIN_URL`** — the IdentityServer Admin UI URL
100+
101+
```
102+
https://app-talent-admin-dev.azurewebsites.net
103+
```
87104

88-
**Retrieve the connection strings from the Bicep outputs:**
105+
**Secrets to add to Key Vault** (not GitHub — these contain sensitive values):
89106

90107
```bash
91-
az deployment group show \
92-
--resource-group rg-talent-dev \
93-
--name main \
94-
--query properties.outputs \
95-
--output table
108+
KV="kv-talent-dev"
109+
110+
# API database connection string
111+
az keyvault secret set --vault-name $KV \
112+
--name "ConnectionStrings--DefaultConnection" \
113+
--value "Server=tcp:sql-talent-dev.database.windows.net,1433;Initial Catalog=sqldb-talent-api-dev;Persist Security Info=False;User ID=sqladmin;Password=<your-password>;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
114+
115+
# IdentityServer database connection string
116+
az keyvault secret set --vault-name $KV \
117+
--name "ConnectionStrings--IdsDbConnection" \
118+
--value "Server=tcp:sql-talent-dev.database.windows.net,1433;Initial Catalog=sqldb-talent-ids-dev;Persist Security Info=False;User ID=sqladmin;Password=<your-password>;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
119+
120+
# JWT signing key
121+
az keyvault secret set --vault-name $KV \
122+
--name "JWTSettings--Key" \
123+
--value "<your-jwt-signing-key>"
96124
```
97125

126+
**⚠️ One-time prerequisite:** Your local Azure CLI identity needs the **Key Vault Secrets Officer** role on `kv-talent-dev` to run `az keyvault secret set`. Grant it in the Portal: **Key Vault → kv-talent-dev → Access control (IAM) → Add role assignment → Key Vault Secrets Officer → your account**.
127+
98128
### Step 2: Understand the Workflow File Structure
99129

100130
Both workflows live in `.github/workflows/` in the parent repository. They follow the same pattern:

blogs/series-5-devops-data/5.8-azure-post-deployment-config.md

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ This article is part of the **AngularNetTutorial** series. The full-stack tutori
3131
**Before following this article, you should have:**
3232

3333
* **Article 5.6 complete** — IdentityServer deployed at `https://app-talent-ids-dev.azurewebsites.net`
34-
* **Article 5.7 complete** — Angular deployed at the Static Web App URL (e.g., `https://agreeable-desert-01234567.azurestaticapps.net`)
34+
* **Article 5.7 complete** — Angular deployed at the Static Web App URL (e.g., `https://mango-flower-0ced4011e.4.azurestaticapps.net`)
3535
* **The three Azure URLs** — retrieve them:
3636

3737
```bash
@@ -58,7 +58,7 @@ az staticwebapp show \
5858

5959
## 🎯 The Problem
6060

61-
OAuth 2.0 authorization codes and tokens can only flow to URLs that are explicitly registered in the authorization server (IdentityServer). If the Angular application running at `https://agreeable-desert-01234567.azurestaticapps.net` sends an authorization request asking IdentityServer to redirect the browser back to that URL, IdentityServer checks its registered `RedirectUris` for the `TalentManagement` client. If the production URL isn't there, IdentityServer rejects the request immediately with `invalid_redirect_uri`.
61+
OAuth 2.0 authorization codes and tokens can only flow to URLs that are explicitly registered in the authorization server (IdentityServer). If the Angular application running at `https://mango-flower-0ced4011e.4.azurestaticapps.net` sends an authorization request asking IdentityServer to redirect the browser back to that URL, IdentityServer checks its registered `RedirectUris` for the `TalentManagement` client. If the production URL isn't there, IdentityServer rejects the request immediately with `invalid_redirect_uri`.
6262

6363
Similarly, the API's CORS policy controls which origins can call the API from a browser. If the Angular app's domain is not in the allowed origins list, the browser blocks the API response before Angular can read it — even though the API returned HTTP 200.
6464

@@ -68,6 +68,8 @@ These two configurations — IdentityServer redirect URIs and API CORS origins
6868

6969
## 💡 The Solution
7070

71+
**Note on connection strings:** Database connection strings were stored in Azure Key Vault in Article 5.4 and wired into App Service settings as `@Microsoft.KeyVault(...)` references by the deploy workflows in Article 5.6. No `appsettings.json` changes are needed for connection strings — this article focuses only on IdentityServer redirect URIs and API CORS origins, which are URLs (not secrets) and are set by the deploy workflows as plain values.
72+
7173
Add the production Static Web App URL to three lists in `identityserverdata.json` (the file that seeds IdentityServer's database):
7274

7375
* `RedirectUris` — where IdentityServer may send authorization codes after login
@@ -123,7 +125,7 @@ Open `TokenService/Duende-IdentityServer/shared/identityserverdata.json`. Find t
123125
}
124126
```
125127

126-
Add the production Azure URLs to each list. Replace `https://agreeable-desert-01234567.azurestaticapps.net` with your actual Static Web App URL:
128+
Add the production Azure URLs to each list. Replace `https://mango-flower-0ced4011e.4.azurestaticapps.net` with your actual Static Web App URL:
127129

128130
```json
129131
{
@@ -136,19 +138,24 @@ Add the production Azure URLs to each list. Replace `https://agreeable-desert-01
136138
"https://localhost:4200/silent-refresh.html",
137139
"http://localhost:4200/callback",
138140
"https://localhost:4200/callback",
139-
"https://agreeable-desert-01234567.azurestaticapps.net",
140-
"https://agreeable-desert-01234567.azurestaticapps.net/silent-refresh.html",
141-
"https://agreeable-desert-01234567.azurestaticapps.net/callback"
141+
"https://mango-flower-0ced4011e.4.azurestaticapps.net",
142+
"https://mango-flower-0ced4011e.4.azurestaticapps.net/silent-refresh.html",
143+
"https://mango-flower-0ced4011e.4.azurestaticapps.net/callback",
144+
"https://workcontrolgit.github.io/AngularNetTutorial",
145+
"https://workcontrolgit.github.io/AngularNetTutorial/silent-refresh.html",
146+
"https://workcontrolgit.github.io/AngularNetTutorial/callback"
142147
],
143148
"PostLogoutRedirectUris": [
144149
"http://localhost:4200",
145150
"https://localhost:4200",
146-
"https://agreeable-desert-01234567.azurestaticapps.net"
151+
"https://mango-flower-0ced4011e.4.azurestaticapps.net",
152+
"https://workcontrolgit.github.io/AngularNetTutorial"
147153
],
148154
"AllowedCorsOrigins": [
149155
"http://localhost:4200",
150156
"https://localhost:4200",
151-
"https://agreeable-desert-01234567.azurestaticapps.net"
157+
"https://mango-flower-0ced4011e.4.azurestaticapps.net",
158+
"https://workcontrolgit.github.io"
152159
]
153160
}
154161
```
@@ -170,23 +177,20 @@ For this tutorial, Option A (commit and push) is preferred — the source file r
170177

171178
### Step 4: Update API CORS Settings
172179

173-
The API allows CORS origins configured in `appsettings.json`. Locate the CORS section:
174-
175-
```bash
176-
grep -n "CorsOrigins\|AllowedOrigins\|Cors" \
177-
ApiResources/TalentManagement-API/TalentManagementAPI.WebApi/appsettings.json
178-
```
179-
180-
Add the Static Web App URL to the allowed origins, then push the change or set it as an App Service setting:
180+
The API allows CORS origins configured via App Service settings using the `Cors__AllowedOrigins__` array pattern. Two origins are needed — the Azure Static Web App and GitHub Pages:
181181

182182
```bash
183183
az webapp config appsettings set \
184184
--resource-group rg-talent-dev \
185185
--name app-talent-api-dev \
186-
--settings "CorsOrigins=https://agreeable-desert-01234567.azurestaticapps.net"
186+
--settings \
187+
"Cors__AllowedOrigins__0=https://mango-flower-0ced4011e.4.azurestaticapps.net" \
188+
"Cors__AllowedOrigins__1=https://workcontrolgit.github.io"
187189
```
188190

189-
The API middleware reads this setting at startup and adds the origin to the allowed list.
191+
**Note:** Replace `https://mango-flower-0ced4011e.4.azurestaticapps.net` with your actual Static Web App URL. The GitHub Pages origin is the host only — no path suffix. This is handled automatically by the `deploy-api.yml` workflow on every deployment, so this manual step is only needed if you are configuring the API outside of a workflow run.
192+
193+
The API's CORS middleware reads `Cors:AllowedOrigins` from `IConfiguration` at startup. The `__` double-underscore in the setting name maps to the `:` separator, and the `__0` / `__1` suffixes create an array in .NET configuration.
190194

191195
### Step 5: Validate Each Layer in Order
192196

@@ -240,12 +244,18 @@ After completing Steps 1–4, run through the Layer 1–6 validation in order. E
240244
**Quick CORS verification from the browser console:**
241245

242246
```javascript
247+
// From Azure SWA
248+
fetch('https://app-talent-api-dev.azurewebsites.net/api/v1/health', {
249+
headers: { 'Origin': 'https://mango-flower-0ced4011e.4.azurestaticapps.net' }
250+
}).then(r => console.log('Status:', r.status, 'CORS:', r.headers.get('access-control-allow-origin')))
251+
252+
// From GitHub Pages
243253
fetch('https://app-talent-api-dev.azurewebsites.net/api/v1/health', {
244-
headers: { 'Origin': 'https://agreeable-desert-01234567.azurestaticapps.net' }
254+
headers: { 'Origin': 'https://workcontrolgit.github.io' }
245255
}).then(r => console.log('Status:', r.status, 'CORS:', r.headers.get('access-control-allow-origin')))
246256
```
247257

248-
Expected: `Status: 200 CORS: https://agreeable-desert-01234567.azurestaticapps.net`
258+
Expected for both: `Status: 200 CORS: <the origin you sent>`
249259

250260
---
251261

0 commit comments

Comments
 (0)