Skip to content

Commit 6d3a23e

Browse files
authored
Merge pull request #201 from microsoftgraph/dkershaw10-msi-as-fic-sample
First draft MSI as a FIC sample
2 parents 6ee8d67 + 5a87b95 commit 6d3a23e

5 files changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Configure a secret-less application to call Microsoft Graph
2+
3+
This template demonstrates how to create a secret-less client application, using a user-assigned managed identity
4+
as the credential (configured as part of the application's federated identity credential).
5+
It also creates other resources, that enable you to test using the application to call Microsoft Graph, all without
6+
any application secret or certificate.
7+
8+
Testing involves using an Azure Automation account runbook, which uses PowerShell cmdlets to acquire an access token for the secret-less application, which is then used to call Microsoft Graph.
9+
10+
Further details on using a user-assigned managed identity as a federated identity credential to enable your apps
11+
to go secret-less, including how it works and sample code to acquire access tokens using various client libraries can be found in the [public documentation][msi-as-fic].
12+
13+
## Details
14+
15+
This template sample deploys the following resources:
16+
17+
1. A user-assigned managed identity
18+
2. An application registration with a federated identity credential (configured to use the user-assigned managed identity)
19+
3. A service principal created from the application
20+
4. \[Optional\] App role assignments to the service principal (to access Microsoft Graph) - requires additional permissions
21+
5. \[Optional\] An Azure Automation Account and runbook to validate the newly created application can call Microsoft Graph without using a secret
22+
23+
### Prerequisites
24+
25+
* A valid **Azure subscription**: If you don't own an Azure subscription, [create a free account](https://azure.microsoft.com/free/) before you begin.
26+
* An **Azure resource group** that you own under a valid Azure subscription.
27+
[Bicep tools for authoring and deployment](https://learn.microsoft.com/graph/templates/quickstart-install-bicep-tools). The minimum required Bicep version is v0.30.3.
28+
* Have the requisite **Microsoft Entra roles** to deploy this template:
29+
30+
* Permissions to create applications. [Users have this permission by default](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#compare-member-and-guest-default-permissions). However, [admins can turn off this default](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#restrict-member-users-default-permissions) in which case you need to be assigned at least the [Application Developer](https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#application-developer) role.
31+
* \[Optional\] Permissions to grant Microsoft Graph app roles to the application. This requires the [Privileged Role Administrator][priv-role-admin]
32+
33+
### Deploy the Bicep template
34+
35+
#### Deploy all resources in the template and go on to end-to-end test
36+
37+
By default, the Bicep template will deploy the five resources listed earlier (managed identity, application, service principal, app role grants to Microsoft Graph, and an automation account and runbook), which will enable end-to-end testing. The default app roles granted to the application are Group.Read.All and Application.Read.All. This requires the signed-in user to have the elevated [Privileged Role Administrator][priv-role-admin] role.
38+
39+
##### Az CLI
40+
41+
```sh
42+
az deployment group create --resource-group <resource-group> --template-file main.bicep
43+
```
44+
45+
##### Az Powershell
46+
47+
```powershell
48+
New-AzResourceGroupDeployment -ResourceGroupName <resource-group> -TemplateFile .\main.bicep
49+
```
50+
51+
#### Deploy app and FIC resources only and forego end-to-end test
52+
53+
If you just want to create the managed identity, application and service principal - and **forego any end-to-end testing within the Azure Automation account**, then set the `graphRoles` parameter to an empty array - i.e. [] in CLI or @() in PowerShell.
54+
55+
##### Az CLI
56+
57+
```sh
58+
az deployment group create --resource-group <YOUR-RESOURCE_GROUP> --template-file main.bicep --parameters graphRoles=[]
59+
```
60+
61+
##### Az Powershell
62+
63+
```powershell
64+
New-AzResourceGroupDeployment -ResourceGroupName <resource-group> -TemplateFile .\main.bicep -graphRoles @()
65+
```
66+
67+
### Test calling Microsoft Graph with a secretless application
68+
69+
>**NOTE**: This testing **assumes** that you have deployed all resources described in the template, as the test relies on using Azure Automation.
70+
71+
Now that the template is deployed, the application can acquire a token for the managed identity and use that token as a credential assertion to acquire an access token to resources like Microsoft Graph. However, in order to acquire a token for the managed identity, the managed identity **must** be assigned in an Azure Cloud Service like a VM, App Services or in our case an Azure Automation account.
72+
73+
1. Sign in to the [Azure Automation Accounts page][auto-accounts] in the Azure Portal. You should see a new Automation Account that was created as part of
74+
the Bicep template deployment.
75+
2. Click on that account, and on the next page click on **Manage a runbook**. This should take you to a page that contains the runbook deployed by the Bicep template. Click on the runbook.
76+
3. On the runbook page, click on **Edit** and select **Edit in portal**. This brings up a page with an empty edit pane.
77+
4. In this sample's GitHub folder find the **secretless-graph-request.ps1** PowerShell script and copy its contents into the empty edit pane in the portal. Click Save. Click on **Test pane** to run and test the PowerShell script. NOTE: the PowerShell script requires the input of four parameters that you can get from the output of the Bicep template deployment.
78+
5. The script acquires a token for the managed identity and then uses it (as a federated token) to sign in to Azure PS as the app. Finally, Azure PS, running as the app, is used to call Microsoft Graph to get the tenant's Entra groups.
79+
80+
If successful, you should see that the script, running as the app, successfully calls Microsoft Graph, responding with the collection of groups in the tenant. And all without requiring the application to have a secret of a certificate.
81+
82+
[msi-as-fic]:https://learn.microsoft.com/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center
83+
[priv-role-admin]:https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#privileged-role-administrator
84+
[az-portal]:https://portal.azure.com
85+
[auto-accounts]:https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/Microsoft.Automation%2FAutomationAccounts
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"experimentalFeaturesEnabled": {
3+
"extensibility": true
4+
},
5+
// specify an alias for the version of the v1.0 dynamic types package you want to use
6+
"extensions": {
7+
"microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.1.8-preview"
8+
}
9+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
extension microsoftGraphV1
2+
3+
// TEMPLATE DESCRIPTION
4+
// Creates a secret-less client application, using a user-assigned managed identity
5+
// as the credential (configured as part of the application's federated identity credential).
6+
// The script optionally
7+
8+
@description('Specifies the name of cloud environment to run this deployment in.')
9+
param cloudEnvironment string = environment().name
10+
11+
// NOTE: Microsoft Graph Bicep file deployment is only supported in Public Cloud
12+
@description('Audience uris for public and national clouds')
13+
param audiences object = {
14+
AzureCloud: {
15+
uri: 'api://AzureADTokenExchange'
16+
}
17+
AzureUSGovernment: {
18+
uri: 'api://AzureADTokenExchangeUSGov'
19+
}
20+
USNat: {
21+
uri: 'api://AzureADTokenExchangeUSNat'
22+
}
23+
USSec: {
24+
uri: 'api://AzureADTokenExchangeUSSec'
25+
}
26+
AzureChinaCloud: {
27+
uri: 'api://AzureADTokenExchangeChina'
28+
}
29+
}
30+
31+
@description('Specifies the resource group location.')
32+
param location string = resourceGroup().location
33+
34+
@description('Specifies the user-assigned managed identity name to use as an application credential via federated identity credentials')
35+
param myWorkloadManagedIdentity string = 'myMSI-2024-12-18'
36+
37+
@description('Specified the application display name')
38+
param applicationDisplayName string = 'myApp-2024-12-18'
39+
40+
@description('Specifies the applications unique name identifier')
41+
param applicationName string = 'myApp-2024-12-18'
42+
43+
@description('Specifies the Microsoft Graph app roles to be granted to the created application. If set to empty array [], app roles will NOT be granted and no Azure Automation accounts will be created.')
44+
param graphRoles array = ['Group.Read.All','Application.Read.All']
45+
46+
@description('Specifies an Azure Automation Account name for a runbook where a PS script can be run. Only created is graphRoles is not an empty array []')
47+
param automationAccountName string = 'myAutomationAccount-2024-12-18'
48+
49+
// login endpoint and tenant ID and issuer
50+
var loginEndpoint = environment().authentication.loginEndpoint
51+
var tenantId = tenant().tenantId
52+
var issuer = '${loginEndpoint}${tenantId}/v2.0'
53+
54+
// create a user assigned managed identity scoped to a resource group
55+
resource myManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
56+
name: myWorkloadManagedIdentity
57+
location: location
58+
}
59+
60+
// Create a (client) application registration with a federated identity credential (FIC)
61+
// The FIC is configured with the managed identity as the subject
62+
// NOTE: app is configured with required properties only. Add the properties your app needs
63+
resource myApp 'Microsoft.Graph/applications@v1.0' = {
64+
displayName: applicationDisplayName
65+
uniqueName: applicationName
66+
67+
resource myMsiFic 'federatedIdentityCredentials@v1.0' = {
68+
name: '${myApp.uniqueName}/msiAsFic'
69+
description: 'Trust the workload\'s user-assigned MI as a credential for the app'
70+
audiences: [
71+
audiences[cloudEnvironment].uri
72+
]
73+
issuer: issuer
74+
subject: myManagedIdentity.properties.principalId
75+
}
76+
}
77+
78+
// Create a service principal for the application
79+
resource mySP 'Microsoft.Graph/servicePrincipals@v1.0' = {
80+
appId: myApp.appId
81+
}
82+
83+
// NOTE: This section (to grant Microsoft Graph permissions) requires an elevated role
84+
// Grant the application only permission to Microsoft Graph
85+
// First find the Microsoft Graph service principal
86+
// Finally assign app roles to the app (if graphRoles is not an empty array)
87+
88+
// 1. find Graph based on well-known appId
89+
resource msGraphSP 'Microsoft.Graph/servicePrincipals@v1.0' existing = {
90+
appId: '00000003-0000-0000-c000-000000000000'
91+
}
92+
93+
// 2. Grant the client app access to Microsoft Graph using the Oauth2 scope
94+
// which delegates the app to act as the sign-in user, constrained by the Oauth2 scope
95+
// This step only happens if the oauth2GraphScope is specified.
96+
97+
// would use a for loop but that appears to be busted for some reason
98+
var graphAppRoles = msGraphSP.appRoles
99+
100+
// Assign multiple app role assignments to MS Graph for the app/SP.
101+
// This gives the app/SP the necessary permissions to deploy this Bicep file (in app-only mode)
102+
resource appRoleAssignments 'Microsoft.Graph/appRoleAssignedTo@v1.0' = [for (role, i) in graphRoles: {
103+
appRoleId: filter(graphAppRoles, graphAppRoles => graphAppRoles.value == role)[0].id
104+
principalId: mySP.id // Client SP being granted permission to access the resource (API)
105+
resourceId: msGraphSP.id // Resource here is Microsoft Graph
106+
}
107+
]
108+
109+
// Create an automation account and runbook to validate created application
110+
// can call Microsoft Graph without using a secret
111+
resource automationAccount 'Microsoft.Automation/automationAccounts@2023-11-01' = if(graphRoles != []) {
112+
name: automationAccountName
113+
identity: {
114+
type:'UserAssigned'
115+
userAssignedIdentities: {
116+
'${resourceGroup().id}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${myWorkloadManagedIdentity}':{}
117+
}
118+
}
119+
location: location
120+
properties: {
121+
sku: {
122+
name: 'Basic'
123+
}
124+
}
125+
resource myRunbook 'runbooks@2023-11-01' = {
126+
name: 'msi-as-fic-test-runbook'
127+
location: location
128+
properties: {
129+
description: 'Runbook for msi-as-fic testing using Az PowerShell'
130+
runbookType: 'PowerShell72'
131+
logProgress: false
132+
logVerbose: false
133+
}
134+
}
135+
}
136+
137+
// outputs
138+
output clientAppId string = myApp.appId
139+
output ficIssuerAudience string = audiences[cloudEnvironment].uri
140+
output issuerURI string = issuer
141+
output tenantId string = tenantId
142+
output assignments array = [ for (role,i) in graphRoles: {
143+
appRoleIDName: appRoleAssignments[i].appRoleId
144+
}]
145+
output miPrincipalId string = myManagedIdentity.properties.principalId
146+
output miClientId string = myManagedIdentity.properties.clientId
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using './main.bicep'
2+
3+
param myWorkloadManagedIdentity = '[MANAGED-IDENTITY-NAME]'
4+
param applicationDisplayName = '[APPLICATION-DISPLAY-NAME]'
5+
param applicationName = '[APPLICATION-UNIQUE-NAME]'
6+
param cloudEnvironment = 'publicCloud'
7+
param graphRoles = ['Group.Read.All']
8+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#######################################################################
2+
<#
3+
Perform a 2-legged flow to acquire an access token with a managed
4+
identity as the credential and then use it to call Microsoft Graph.
5+
6+
PRE-REQUISITES:
7+
1. An app registered with a user-assigned managed identity as a
8+
federated identity credential (FIC).
9+
2. There's a service principal for the app.
10+
3. The service principal is is granted access to Microsoft Graph.
11+
12+
SCRIPT STEPS
13+
1. Acquire a token for a user-assigned managed identity
14+
2. Run PS as an app, using the token from step 1 as the credential
15+
3. Call Microsoft Graph
16+
#>
17+
#######################################################################
18+
19+
param
20+
(
21+
[Parameter(Mandatory=$true)]
22+
$managedIdentityPrincipalId,
23+
$applicationClientId,
24+
$tenantId
25+
$ficIssuerAudience
26+
)
27+
28+
# Step 1: Acquire token for the managed identity
29+
Connect-AzAccount -Identity -AccountId $managedIdentityPrincipalId
30+
$token = Get-AzAccessToken -ResourceUrl $ficIssuerAudience
31+
32+
33+
# Step 2: Sign in to Azure PowerShell (as the app with the FIC configuration)
34+
Connect-AzAccount -ApplicationId $applicationClientId -FederatedToken $token.Token -Tenant $tenantId
35+
36+
# Step 3: Get all Entra groups in the tenant (assumes app has Group.Read.All permission)
37+
Invoke-AzRestMethod -Method GET -Uri https://graph.microsoft.com/v1.0/groups

0 commit comments

Comments
 (0)