Skip to content

Commit cc016c7

Browse files
emmanuelknafoCopilot
andcommitted
Enhance Entra ID setup script and documentation for Evidence Portal workshop
- Updated PowerShell script to provide a comprehensive, idempotent setup for Entra ID app registrations, including detailed steps for creating API and SPA apps, configuring OAuth2 scopes, app roles, and service principals. - Improved parameter descriptions and added optional parameters for production redirect URI and API scope name. - Enhanced error handling and logging for better user feedback during execution. - Revised workshop guides to reflect the new script capabilities, emphasizing the automated setup process and its benefits. - Added a fast-track deployment option in the Azure deployment guide, streamlining the process for users to deploy the entire solution in one command. Co-authored-by: Copilot <copilot@github.com>
1 parent 2e17577 commit cc016c7

13 files changed

Lines changed: 1014 additions & 298 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ replay_pid*
5151
.env.development.local
5252
.env.test.local
5353
.env.production.local
54+
.entra-apps.json
55+
.deploy-output.json
5456

5557
# --- OS ---
5658
.DS_Store

README.md

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,49 +87,109 @@ The sample apps work immediately without any Azure or Entra ID configuration. Th
8787
8888
### Bootstrap Entra ID App Registrations (PowerShell)
8989

90-
[scripts/setup-entra-apps.ps1](scripts/setup-entra-apps.ps1) is an idempotent PowerShell helper that creates the SPA and API app registrations against the tenant you are currently logged in to with the Azure CLI. It is the fastest path through Exercise 1 if you prefer scripting over the Azure Portal.
90+
[scripts/setup-entra-apps.ps1](scripts/setup-entra-apps.ps1) is an idempotent PowerShell helper that creates and fully configures the SPA and API app registrations against the tenant you are currently logged in to with the Azure CLI. It is the fastest path through Exercise 1 if you prefer scripting over the Azure Portal.
9191

92-
What it does today (Phase 1):
92+
What it does (every call is a no-op if the resource is already configured the right way):
9393

9494
- Verifies `az` is installed and you are signed in (`az login`).
9595
- Acquires a Microsoft Graph access token and calls Graph directly via `Invoke-RestMethod` (no `az rest` quoting issues on Windows).
96-
- Creates the **API app** and sets its Application ID URI to `api://<appId>`.
97-
- Creates the **SPA app** and configures its SPA platform redirect URI (default `http://localhost:4200`).
98-
- On re-run, looks each app up by `displayName` and reuses it instead of creating duplicates. Every step is a no-op if already configured.
96+
- Creates the **API app**, sets its Application ID URI to `api://<appId>`, exposes the `Evidence.Read` OAuth2 scope, and defines the `CaseReader` and `CaseAdmin` app roles.
97+
- Creates the **SPA app**, configures its SPA platform redirect URI(s), grants the delegated `Evidence.Read` permission, and pre-authorizes the SPA on the API.
98+
- Creates service principals for both apps if they don't exist yet.
99+
- (Optional, default on) Grants tenant admin consent for the SPA's delegated permission and self-assigns the signed-in user to both `CaseReader` and `CaseAdmin` so you can sign in immediately.
100+
- (Optional, default on) Patches the local `environment.ts`, `environment.prod.ts`, and `application.properties` files with the resulting client/tenant IDs and scope URI.
99101

100102
Usage:
101103

102104
```powershell
103105
# Sign in to the tenant where the apps should live
104106
az login --tenant <tenantId>
105107
106-
# Bootstrap both app registrations
108+
# Bootstrap both app registrations and patch local config
107109
.\scripts\setup-entra-apps.ps1 `
108110
-SpaName "Evidence Portal SPA" `
109111
-ApiName "Evidence Portal API"
110112
111-
# Optional: capture the resulting IDs for downstream automation (e.g. deploy.ps1)
113+
# Re-run later with a production redirect URI (idempotent)
112114
.\scripts\setup-entra-apps.ps1 `
113115
-SpaName "Evidence Portal SPA" `
114116
-ApiName "Evidence Portal API" `
115-
-RedirectUri "https://my-spa.azurewebsites.net" `
117+
-ProductionRedirectUri "https://my-spa.azurewebsites.net" `
116118
-OutputFile ".\.entra-apps.json"
117119
```
118120

119-
The script returns and prints `tenantId`, `apiAppId`, `apiObjectId`, `identifierUri`, `spaAppId`, `spaObjectId`, and `redirectUri`. Plug `tenantId`, `apiAppId`, and `spaAppId` into [`environment.ts`](sample-app/spa/src/environments/environment.ts) and [`application.properties`](sample-app/api/src/main/resources/application.properties) (or pass them to [scripts/deploy.ps1](scripts/deploy.ps1)).
121+
The script returns and prints `tenantId`, `apiAppId`, `apiObjectId`, `apiServicePrincipalId`, `apiScopeId`, `apiScopeUri`, `roleReaderId`, `roleAdminId`, `spaAppId`, `spaObjectId`, `spaServicePrincipalId`, plus the redirect URIs and consent/role-assignment status. With `-OutputFile` it also writes a JSON state file that [scripts/deploy.ps1](scripts/deploy.ps1) consumes on its next run, so you don't need to re-run setup before every deployment.
122+
123+
> Skip the patching or admin consent with `-UpdateLocalConfig:$false`, `-GrantAdminConsent:$false`, or `-AssignCurrentUserToRoles:$false` if you would rather wire those up by hand.
124+
125+
### Fast-Track to Azure (One Command)
126+
127+
If you want to see the deployed end-state in Azure as quickly as possible — without going through the four guided exercises — run the one-stop deployment script. It chains every step of Exercises 1 and 4 into a single idempotent run.
128+
129+
```powershell
130+
# Sign in once to the tenant where the apps and Azure resources should live
131+
az login --tenant <tenantId>
132+
az account set --subscription <subscriptionIdOrName>
133+
134+
# Deploy everything (Entra ID + Bicep + SPA + API + evidence files)
135+
.\scripts\deploy.ps1
136+
```
137+
138+
What `deploy.ps1` does end-to-end:
139+
140+
1. Verifies `az`, `node`, `mvn` (auto-installs Maven into `%LOCALAPPDATA%\Maven` if missing).
141+
2. Calls `setup-entra-apps.ps1` to create/reuse both app registrations, expose the scope and roles, grant admin consent, and assign your user to `CaseReader` + `CaseAdmin`.
142+
3. Creates the resource group `rg-evidence-workshop` in `canadacentral` and a deterministic globally-unique storage account name.
143+
4. Deploys the Bicep stack (App Service Plan, two App Services with system-assigned Managed Identity, Storage Account, Application Insights, role assignments).
144+
5. Patches `environment.prod.ts` with the deployed SPA/API URLs and App Insights connection string.
145+
6. Re-runs `setup-entra-apps.ps1` to add the production SPA URL as a SPA-platform redirect URI on the SPA app registration.
146+
7. Builds the Angular SPA in production mode and the Spring Boot API as an executable JAR.
147+
8. Deploys the SPA zip and the API JAR with `az webapp deploy`.
148+
9. Grants your user `Storage Blob Data Contributor` on the storage account, creates the `evidence` container, and uploads the five sample PDFs (with retries to absorb RBAC propagation).
149+
10. Smoke-tests the result: SPA URL must return `200`, API `/api/cases` must return `401` (proving JWT validation is enforced).
150+
151+
When it finishes you'll see something like:
152+
153+
```text
154+
Deployment complete
155+
156+
Resource Group : rg-evidence-workshop
157+
Region : canadacentral
158+
SPA URL : https://app-evidence-spa-workshop.azurewebsites.net
159+
API URL : https://app-evidence-api-workshop.azurewebsites.net
160+
Storage : stevpworkshopXXXXXXXX (container: evidence)
161+
```
162+
163+
Open the SPA URL, sign in with the same account you ran the script as, and you should land on the case list with all five sample cases — files served from Blob Storage through the API's Managed Identity.
164+
165+
Common flags:
166+
167+
| Flag | Default | Purpose |
168+
|---|---|---|
169+
| `-ResourceGroup` | `rg-evidence-workshop` | Target resource group (created if missing). |
170+
| `-Location` | `canadacentral` | Azure region. |
171+
| `-Environment` | `workshop` | Suffix used for App Service names (`app-evidence-spa-<env>`, `app-evidence-api-<env>`). |
172+
| `-SkipEntraSetup` | off | Reuse a previous `.entra-apps.json` and skip the Graph calls. |
173+
| `-SkipBuild` | off | Reuse the existing `dist/` and `target/` artifacts. |
174+
| `-SkipUpload` | off | Skip the sample-evidence blob upload. |
175+
176+
When you're done with the workshop, remove everything with:
177+
178+
```powershell
179+
az group delete --name rg-evidence-workshop --yes --no-wait
180+
```
120181

121-
> **Phase 2 (planned):** the same script will be extended via Microsoft Graph to expose the `Evidence.Read` scope, define `CaseReader` / `CaseAdmin` app roles, add the SPA's delegated permission on the API, pre-authorize the SPA, and grant tenant admin consent. Until then, complete those steps in the Azure Portal as described in [Exercise 1](workshop/guides/exercise-1-app-registrations.md).
122182

123183
### Workshop Exercises
124184

125-
Follow these exercises in order for the full 3-hour workshop experience:
185+
Follow these exercises in order for the full 3-hour workshop experience. Already saw the Fast-Track land everything in Azure? You can still use these guides as a tear-down of what `deploy.ps1` automated.
126186

127187
| Exercise | Duration | Description |
128188
|---|---|---|
129-
| [Exercise 1: Configure App Registrations](workshop/guides/exercise-1-app-registrations.md) | 30 min | Create Entra ID app registrations for the SPA and API, configure scopes, roles, and update the SPA environment |
130-
| [Exercise 2: Run SPA + API Locally](workshop/guides/exercise-2-run-locally.md) | 30 min | Sign in through the SPA, browse cases, download evidence, and inspect JWT tokens |
131-
| [Exercise 3: Add Role-Protected Endpoint](workshop/guides/exercise-3-add-endpoint.md) | 20 min | Experience the RBAC cycle: 403 Forbidden, assign CaseAdmin role, re-authenticate, 201 Created |
132-
| [Exercise 4: Deploy to Azure](workshop/guides/exercise-4-deploy-azure.md) | 20 min | Deploy both apps and infrastructure to Azure using Bicep, verify Managed Identity storage access |
189+
| [Exercise 1: Configure App Registrations](workshop/guides/exercise-1-app-registrations.md) | 30 min | Create Entra ID app registrations for the SPA and API, configure scopes, roles, and update the SPA environment. (Automated end-to-end by `setup-entra-apps.ps1`.) |
190+
| [Exercise 2: Run SPA + API Locally](workshop/guides/exercise-2-run-locally.md) | 30 min | Sign in through the SPA, browse cases, download evidence, and inspect JWT tokens. |
191+
| [Exercise 3: Add Role-Protected Endpoint](workshop/guides/exercise-3-add-endpoint.md) | 20 min | Experience the RBAC cycle: 403 Forbidden, assign CaseAdmin role, re-authenticate, 201 Created. |
192+
| [Exercise 4: Deploy to Azure](workshop/guides/exercise-4-deploy-azure.md) | 20 min | Deploy both apps and infrastructure to Azure using Bicep, verify Managed Identity storage access. (Automated end-to-end by `deploy.ps1`.) |
133193

134194
For the full instructor delivery guide with 9-module schedule and presentation notes, see [workshop/README.md](workshop/README.md).
135195

infra/main.bicep

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ module apiApp 'modules/app-service-api.bicep' = {
105105
tenantId: tenantId
106106
storageAccountName: storageAccountName
107107
appInsightsConnectionString: monitoring.outputs.appInsightsConnectionString
108+
allowedOrigin: 'https://${spaAppName}.azurewebsites.net'
108109
tags: tags
109110
}
110111
}
@@ -130,5 +131,14 @@ output spaUrl string = 'https://${spaAppName}.azurewebsites.net'
130131
@description('URL of the deployed API application.')
131132
output apiUrl string = 'https://${apiAppName}.azurewebsites.net'
132133

134+
@description('SPA App Service name.')
135+
output spaAppName string = spaAppName
136+
137+
@description('API App Service name.')
138+
output apiAppName string = apiAppName
139+
133140
@description('Storage account name for evidence blob container.')
134141
output storageAccountNameOutput string = storageAccountName
142+
143+
@description('Application Insights connection string.')
144+
output appInsightsConnectionString string = monitoring.outputs.appInsightsConnectionString

infra/main.bicepparam

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
// ---------------------------------------------------------------------------
2+
// main.bicepparam — Default parameters for the Evidence Portal infrastructure.
3+
//
4+
// IMPORTANT: deploy.ps1 overrides storageAccountName, spaClientId, apiClientId,
5+
// and tenantId via --parameters key=value. The placeholder values below are
6+
// only used when running `az deployment group create --parameters main.bicepparam`
7+
// directly. Replace them or pass overrides on the command line.
8+
// ---------------------------------------------------------------------------
9+
110
using './main.bicep'
211

3-
param environmentName = 'workshop'
4-
param location = 'canadacentral'
12+
param environmentName = 'workshop'
13+
param location = 'canadacentral'
514
param appServicePlanSku = 'B1'
15+
16+
// Override these on the CLI: --parameters storageAccountName=... spaClientId=... etc.
17+
param storageAccountName = 'REPLACEME'
18+
param spaClientId = '00000000-0000-0000-0000-000000000000'
19+
param apiClientId = '00000000-0000-0000-0000-000000000000'
20+
param tenantId = '00000000-0000-0000-0000-000000000000'

infra/modules/app-service-api.bicep

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ param storageAccountName string
2323
@description('Application Insights connection string.')
2424
param appInsightsConnectionString string
2525

26+
@description('Allowed origin for CORS (typically the SPA App Service URL).')
27+
param allowedOrigin string
28+
2629
@description('Resource tags.')
2730
param tags object = {}
2831

@@ -46,6 +49,10 @@ resource apiApp 'Microsoft.Web/sites@2023-12-01' = {
4649
name: 'WEBSITES_PORT'
4750
value: '8080'
4851
}
52+
{
53+
name: 'SPRING_PROFILES_ACTIVE'
54+
value: 'prod'
55+
}
4956
{
5057
name: 'SPRING_CLOUD_AZURE_ACTIVE_DIRECTORY_CREDENTIAL_CLIENT_ID'
5158
value: apiClientId
@@ -58,6 +65,18 @@ resource apiApp 'Microsoft.Web/sites@2023-12-01' = {
5865
name: 'SPRING_CLOUD_AZURE_ACTIVE_DIRECTORY_APP_ID_URI'
5966
value: 'api://${apiClientId}'
6067
}
68+
{
69+
name: 'JWT_ISSUER_URI'
70+
value: '${environment().authentication.loginEndpoint}${tenantId}/v2.0'
71+
}
72+
{
73+
name: 'JWT_AUDIENCE'
74+
value: 'api://${apiClientId}'
75+
}
76+
{
77+
name: 'AZURE_TENANT_ID'
78+
value: tenantId
79+
}
6180
{
6281
name: 'AZURE_STORAGE_ACCOUNT_NAME'
6382
value: storageAccountName
@@ -66,6 +85,10 @@ resource apiApp 'Microsoft.Web/sites@2023-12-01' = {
6685
name: 'AZURE_STORAGE_CONTAINER_NAME'
6786
value: 'evidence'
6887
}
88+
{
89+
name: 'CORS_ALLOWED_ORIGINS'
90+
value: allowedOrigin
91+
}
6992
{
7093
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
7194
value: appInsightsConnectionString

sample-app/api/src/main/resources/application.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ server.port=8080
44
# Entra ID / JWT validation
55
# Participants set their tenant ID in Exercise 1
66
spring.security.oauth2.resourceserver.jwt.issuer-uri=${JWT_ISSUER_URI:}
7-
spring.security.oauth2.resourceserver.jwt.audiences=${JWT_AUDIENCE:api://YOUR_API_CLIENT_ID}
7+
spring.security.oauth2.resourceserver.jwt.audiences=${JWT_AUDIENCE:api://3186f769-2e40-47e2-905b-5b9e784d0ae2}
88

99
# CORS
1010
app.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:4200}

sample-app/spa/angular.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,8 @@
8989
}
9090
}
9191
}
92+
},
93+
"cli": {
94+
"analytics": false
9295
}
9396
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
export const environment = {
22
production: true,
33
msalConfig: {
4-
clientId: 'YOUR_SPA_CLIENT_ID',
5-
tenantId: 'YOUR_TENANT_ID',
6-
redirectUri: 'https://YOUR_SPA_APP_NAME.azurewebsites.net',
4+
clientId: '3fd702c1-b1c1-427f-a24e-700f841813d3',
5+
tenantId: 'aa93b9d9-037d-4f08-a26d-783cff0e2369',
6+
redirectUri: 'https://app-evidence-spa-workshop.azurewebsites.net',
77
},
88
apiConfig: {
9-
baseUrl: 'https://YOUR_API_APP_NAME.azurewebsites.net/api',
10-
scopes: ['api://YOUR_API_CLIENT_ID/Evidence.Read'],
9+
baseUrl: 'https://app-evidence-api-workshop.azurewebsites.net/api',
10+
scopes: ['api://3186f769-2e40-47e2-905b-5b9e784d0ae2/Evidence.Read'],
1111
},
1212
appInsights: {
13-
connectionString: 'YOUR_CONNECTION_STRING',
13+
connectionString: 'InstrumentationKey=970e6698-ee80-475a-b83c-1e1fc5188612;IngestionEndpoint=https://canadacentral-1.in.applicationinsights.azure.com/;LiveEndpoint=https://canadacentral.livediagnostics.monitor.azure.com/;ApplicationId=6489213d-324d-429c-b1a8-db358f414b85',
1414
},
1515
};

sample-app/spa/src/environments/environment.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
export const environment = {
22
production: false,
33
msalConfig: {
4-
clientId: 'YOUR_SPA_CLIENT_ID',
5-
tenantId: 'YOUR_TENANT_ID',
4+
clientId: '3fd702c1-b1c1-427f-a24e-700f841813d3',
5+
tenantId: 'aa93b9d9-037d-4f08-a26d-783cff0e2369',
66
redirectUri: 'http://localhost:4200',
77
},
88
apiConfig: {
99
baseUrl: '/api',
10-
scopes: ['api://YOUR_API_CLIENT_ID/Evidence.Read'],
10+
scopes: ['api://3186f769-2e40-47e2-905b-5b9e784d0ae2/Evidence.Read'],
1111
},
1212
appInsights: {
1313
connectionString: '',

0 commit comments

Comments
 (0)