Skip to content

Commit 075a9ee

Browse files
paulyukCopilotCopilotjongiopaulyufan
authored
fix(app-service-templates): address review feedback — discoverability, auth IaC, token refresh, source splits (#1635)
* feat(develop): App Service templates + composition algorithm (Gap-1) 11 template files for App Service development scaffolding: - selection.md decision tree - web-api.md and web-app.md base templates - Composition algorithm (recipes for SQL, Cosmos, Auth, Redis) - Language-specific source (C#, Python) Closes #1609 Parent: #1608 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(app-service-templates): address PR review feedback - discoverability, bicep schema tools, auth IaC classification, token refresh, source splits Agent-Logs-Url: https://github.com/microsoft/GitHub-Copilot-for-Azure/sessions/d8b94e2b-3785-456d-8309-ed9a3a5b446c Co-authored-by: jongio <2163001+jongio@users.noreply.github.com> * fix: address jongio's runtime-failure feedback on app-service recipes - sql/source/python.md: replace one-shot token in attrs_before with SQLAlchemy do_connect event listener that fetches a fresh MI token per physical connection, so the pool stays valid past 1 hour. Also handle empty AZURE_CLIENT_ID by omitting client_id. - redis/source/python.md: replace module-level singleton with get_cache() lazy/refreshing accessor; uses RLock to avoid deadlock with _get_token() and closes the prior client on rotation. - auth/source/nodejs.md: pin jwt.verify() to algorithms: ['RS256'] to match the Python version and prevent algorithm confusion. - sql/source/nodejs.md: validate req.body for the POST /api/todos endpoint — require non-empty trimmed title (string) and validate isComplete is boolean if provided. - composition.md: add per-language env-var table before the .NET ADO.NET connection-string example so Python/Node.js get the correct config (AZURE_SQL_SERVER+AZURE_SQL_DATABASE for Python, DATABASE_URL with sqlserver:// for Node/Prisma). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jongio <2163001+jongio@users.noreply.github.com> Co-authored-by: Paul Yuknewicz <paulyu@microsoft.com>
1 parent 771a666 commit 075a9ee

22 files changed

Lines changed: 2010 additions & 0 deletions

File tree

plugin/skills/azure-prepare/references/services/app-service/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ siteConfig: {
6969

7070
Endpoint should return 200 OK when healthy.
7171

72+
## Templates
73+
74+
For App Service templates with composable recipes (SQL, Cosmos DB, Auth, Redis), see [Template Selection](templates/selection.md).
75+
7276
## Common Data Backends
7377

7478
When pairing App Service with a data layer, load the corresponding service references:
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# App Service Template Recipes — REFERENCE ONLY
2+
3+
Composable IaC + source code modules that extend the Web API or Web App base template to support specific Azure service integrations.
4+
5+
## Architecture
6+
7+
```
8+
Base Template (per language/scenario, from AZD gallery)
9+
10+
├── Source code (REST API or full-stack web app)
11+
├── IaC (App Service Plan, App Service, App Insights, UAMI, RBAC)
12+
└── AZD config (azure.yaml, parameters)
13+
14+
+ Recipe (per integration)
15+
16+
├── Source code delta (service client, middleware, config)
17+
├── IaC delta (new resource + RBAC + networking modules)
18+
└── App settings delta
19+
20+
= Complete deployable project → `azd up`
21+
```
22+
23+
## Available Recipes
24+
25+
| Recipe | IaC Delta? | Source Delta? | Status |
26+
|--------|-----------|--------------|--------|
27+
| [sql](sql/README.md) | ✅ SQL Server + DB + firewall + RBAC | ✅ EF Core (.NET), SQLAlchemy (Python), Prisma (Node.js) · ⏳ Spring Data JPA (Java): planned | ✅ Available |
28+
| [cosmos](cosmos/README.md) | ✅ Cosmos account + DB + container + RBAC + PE | ✅ Cosmos SDK (.NET, Python, Node.js) · ⏳ Spring Data Cosmos (Java): planned | ✅ Available |
29+
| [auth](auth/README.md) | ✅ App registration + Easy Auth config | ✅ MSAL / Identity middleware (.NET, Python, Node.js) · ⏳ Spring Security (Java): planned | ✅ Available |
30+
| [redis](redis/README.md) | ✅ Redis cache + RBAC + PE | ✅ Distributed cache client (.NET, Python, Node.js) · ⏳ Spring Session (Java): planned | ✅ Available |
31+
32+
## How It Works
33+
34+
### Step 1: Fetch Base Template
35+
36+
```bash
37+
# Pick template by language + scenario (see selection.md)
38+
azd init -t <template> -e "$ENV_NAME" --no-prompt
39+
```
40+
41+
### Step 2: Apply Recipe
42+
43+
The skill reads the recipe's README.md for:
44+
- **IaC module files** to copy into `infra/`
45+
- **App settings** to add to web app configuration
46+
- **RBAC roles** with exact GUIDs (never generated by LLM)
47+
- **Source code** to add alongside existing application code
48+
- **Networking** to add private endpoints (conditional on VNET_ENABLED)
49+
50+
### Step 3: Wire Into Base
51+
52+
**Bicep:** Add `module` reference in `main.bicep`, pass `appServicePrincipalId`
53+
**Terraform:** Copy `.tf` file into `infra/`, merge locals into web app settings
54+
55+
### Step 4: Deploy
56+
57+
```bash
58+
azd env set AZURE_LOCATION eastus2
59+
azd provision --no-prompt
60+
sleep 60
61+
azd deploy --no-prompt
62+
```
63+
64+
## Design Principles
65+
66+
| Principle | Why |
67+
|-----------|-----|
68+
| **Never synthesize base IaC** | Always use proven AZD template repos |
69+
| **Never modify base; only extend** | Recipes are additive — no risk of breaking core |
70+
| **Recipes own their RBAC** | Exact role GUIDs, no LLM guessing |
71+
| **Managed identity by default** | No passwords or connection strings in app settings |
72+
| **Health checks required** | Every app must expose `/health` |
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Entra ID / Easy Auth Recipe — REFERENCE ONLY
2+
3+
Adds authentication and authorization to an App Service base template using Microsoft Entra ID.
4+
5+
## Overview
6+
7+
This recipe configures authentication for App Service apps using either Easy Auth (built-in authentication) or MSAL SDK-based authentication. Easy Auth requires zero code changes; MSAL gives full control.
8+
9+
## Integration Type
10+
11+
| Aspect | Value |
12+
|--------|-------|
13+
| **Provider** | Microsoft Entra ID (Azure AD) |
14+
| **Method** | Easy Auth (built-in) or MSAL SDK |
15+
| **Protocols** | OpenID Connect, OAuth 2.0 |
16+
| **Token validation** | Automatic (Easy Auth) or middleware (MSAL) |
17+
18+
## Option A: Easy Auth (Recommended for most apps)
19+
20+
Zero-code authentication built into App Service. Handles login, token management, and session cookies.
21+
22+
### Bicep Configuration
23+
24+
> 💡 Call `mcp_bicep_get_az_resource_type_schema` with resource type `Microsoft.Web/sites/config` to validate properties before generating this resource.
25+
26+
```bicep
27+
resource authSettings 'Microsoft.Web/sites/config@2023-12-01' = {
28+
parent: webApp
29+
name: 'authsettingsV2'
30+
properties: {
31+
globalValidation: {
32+
requireAuthentication: true
33+
unauthenticatedClientAction: 'RedirectToLoginPage'
34+
}
35+
identityProviders: {
36+
azureActiveDirectory: {
37+
enabled: true
38+
registration: {
39+
openIdIssuer: 'https://login.microsoftonline.com/${tenant().tenantId}/v2.0'
40+
clientId: appRegistration.properties.appId
41+
}
42+
validation: {
43+
defaultAuthorizationPolicy: {
44+
allowedApplications: []
45+
}
46+
}
47+
}
48+
}
49+
login: {
50+
tokenStore: {
51+
enabled: true
52+
}
53+
}
54+
}
55+
}
56+
```
57+
58+
### App Registration
59+
60+
> 💡 Call `mcp_bicep_get_az_resource_type_schema` with resource type `Microsoft.Graph/applications` to validate properties before generating this resource. The `microsoftGraphV1_0` extension is required — declare it at the top of the Bicep file.
61+
62+
```bicep
63+
extension microsoftGraphV1_0
64+
65+
resource appRegistration 'Microsoft.Graph/applications@v1.0' = {
66+
displayName: '${name}-app'
67+
web: {
68+
redirectUris: [
69+
'https://${webApp.properties.defaultHostName}/.auth/login/aad/callback'
70+
]
71+
}
72+
}
73+
```
74+
75+
## Option B: MSAL SDK (Full control)
76+
77+
Use when you need custom token validation, API-only auth, or multi-tenant support.
78+
79+
| Language | Source File |
80+
|----------|-------------|
81+
| C# (ASP.NET Core) | [source/dotnet.md](source/dotnet.md) |
82+
| Python (FastAPI) | [source/python.md](source/python.md) |
83+
| Node.js (Express) | [source/nodejs.md](source/nodejs.md) |
84+
85+
## App Settings
86+
87+
| Setting | Value | Purpose |
88+
|---------|-------|---------|
89+
| `AZURE_TENANT_ID` | Entra tenant ID | Identity provider |
90+
| `AZURE_CLIENT_ID` | App registration client ID | Application identity |
91+
92+
## References
93+
94+
- [Easy Auth overview](https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization)
95+
- [Microsoft Identity Web](https://learn.microsoft.com/en-us/entra/msal/dotnet/microsoft-identity-web/)
96+
- [Configure Entra ID auth](https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Auth Recipe — C# (.NET) — REFERENCE ONLY
2+
3+
## Microsoft Identity Web (ASP.NET Core)
4+
5+
### NuGet Packages
6+
7+
```xml
8+
<PackageReference Include="Microsoft.Identity.Web" Version="3.*" />
9+
```
10+
11+
### Program.cs (additions)
12+
13+
Add these lines — do NOT replace the existing file:
14+
15+
```csharp
16+
using Microsoft.Identity.Web;
17+
18+
// Add after builder creation
19+
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
20+
builder.Services.AddAuthorization();
21+
22+
// Add after app build
23+
app.UseAuthentication();
24+
app.UseAuthorization();
25+
26+
// Protected endpoint
27+
app.MapGet("/api/me", [Authorize] (HttpContext ctx) =>
28+
{
29+
var name = ctx.User.FindFirst("name")?.Value;
30+
return Results.Ok(new { name });
31+
});
32+
```
33+
34+
### appsettings.json
35+
36+
```json
37+
{
38+
"AzureAd": {
39+
"Instance": "https://login.microsoftonline.com/",
40+
"TenantId": "<tenant-id>",
41+
"ClientId": "<client-id>",
42+
"Audience": "api://<client-id>"
43+
}
44+
}
45+
```
46+
47+
> ⚠️ The `Audience` must match the Application ID URI of the app registration (typically `api://<client-id>`). Set `AZURE_CLIENT_ID` and `AZURE_TENANT_ID` as app settings in Azure rather than hardcoding them.
48+
49+
## Files to Modify
50+
51+
| File | Action |
52+
|------|--------|
53+
| `Program.cs` | Add authentication middleware + protected endpoints |
54+
| `appsettings.json` | Add AzureAd configuration section |
55+
| `*.csproj` | Add Microsoft.Identity.Web NuGet package |
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Auth Recipe — Node.js (Express) — REFERENCE ONLY
2+
3+
## JWT Validation with jsonwebtoken + jwks-rsa
4+
5+
### npm Packages
6+
7+
```bash
8+
npm install jsonwebtoken jwks-rsa
9+
```
10+
11+
### Auth Middleware
12+
13+
Add `middleware/auth.js`:
14+
15+
```javascript
16+
const jwt = require("jsonwebtoken");
17+
const jwksClient = require("jwks-rsa");
18+
19+
const client = jwksClient({
20+
jwksUri: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/discovery/v2.0/keys`,
21+
cache: true,
22+
rateLimit: true,
23+
});
24+
25+
// Use APP_ID_URI if set (e.g., "api://<client-id>"); fall back to CLIENT_ID
26+
const AUDIENCE = process.env.AZURE_APP_ID_URI || process.env.AZURE_CLIENT_ID;
27+
28+
function authMiddleware(req, res, next) {
29+
const token = req.headers.authorization?.split(" ")[1];
30+
if (!token) return res.status(401).json({ error: "No token" });
31+
32+
const decoded = jwt.decode(token, { complete: true });
33+
if (!decoded || !decoded.header || !decoded.header.kid) {
34+
return res.status(401).json({ error: "Invalid token" });
35+
}
36+
37+
client.getSigningKey(decoded.header.kid, (err, key) => {
38+
if (err || !key) {
39+
return res.status(401).json({ error: "Invalid token" });
40+
}
41+
42+
jwt.verify(
43+
token,
44+
key.getPublicKey(),
45+
{
46+
algorithms: ["RS256"],
47+
audience: AUDIENCE,
48+
issuer: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`,
49+
},
50+
(err, payload) => {
51+
if (err) return res.status(401).json({ error: "Invalid token" });
52+
req.user = payload;
53+
next();
54+
}
55+
);
56+
});
57+
}
58+
59+
module.exports = { authMiddleware };
60+
```
61+
62+
> ⚠️ The `aud` claim in Entra ID tokens is often the Application ID URI (`api://<client-id>`), not the raw client ID. Set `AZURE_APP_ID_URI` in app settings to match your app registration's exposed API URI.
63+
64+
### Protected Endpoint
65+
66+
Add to `src/index.js`:
67+
68+
```javascript
69+
const { authMiddleware } = require("./middleware/auth");
70+
71+
app.get("/api/me", authMiddleware, (req, res) => {
72+
res.json({ name: req.user?.name, oid: req.user?.oid });
73+
});
74+
```
75+
76+
## App Settings Required
77+
78+
| Setting | Value |
79+
|---------|-------|
80+
| `AZURE_TENANT_ID` | Entra tenant ID |
81+
| `AZURE_CLIENT_ID` | App registration client ID |
82+
| `AZURE_APP_ID_URI` | Application ID URI (e.g., `api://<client-id>`) — optional, defaults to CLIENT_ID |
83+
84+
## Files to Modify
85+
86+
| File | Action |
87+
|------|--------|
88+
| `middleware/auth.js` | Create — JWT validation middleware |
89+
| `src/index.js` | Modify — add protected routes |
90+
| `package.json` | Modify — add jsonwebtoken, jwks-rsa |

0 commit comments

Comments
 (0)