-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #201 from microsoftgraph/dkershaw10-msi-as-fic-sample
First draft MSI as a FIC sample
- Loading branch information
Showing
5 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Configure a secret-less application to call Microsoft Graph | ||
|
||
This template demonstrates how to create a secret-less client application, using a user-assigned managed identity | ||
as the credential (configured as part of the application's federated identity credential). | ||
It also creates other resources, that enable you to test using the application to call Microsoft Graph, all without | ||
any application secret or certificate. | ||
|
||
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. | ||
|
||
Further details on using a user-assigned managed identity as a federated identity credential to enable your apps | ||
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]. | ||
|
||
## Details | ||
|
||
This template sample deploys the following resources: | ||
|
||
1. A user-assigned managed identity | ||
2. An application registration with a federated identity credential (configured to use the user-assigned managed identity) | ||
3. A service principal created from the application | ||
4. \[Optional\] App role assignments to the service principal (to access Microsoft Graph) - requires additional permissions | ||
5. \[Optional\] An Azure Automation Account and runbook to validate the newly created application can call Microsoft Graph without using a secret | ||
|
||
### Prerequisites | ||
|
||
* A valid **Azure subscription**: If you don't own an Azure subscription, [create a free account](https://azure.microsoft.com/free/) before you begin. | ||
* An **Azure resource group** that you own under a valid Azure subscription. | ||
[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. | ||
* Have the requisite **Microsoft Entra roles** to deploy this template: | ||
|
||
* 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. | ||
* \[Optional\] Permissions to grant Microsoft Graph app roles to the application. This requires the [Privileged Role Administrator][priv-role-admin] | ||
|
||
### Deploy the Bicep template | ||
|
||
#### Deploy all resources in the template and go on to end-to-end test | ||
|
||
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. | ||
|
||
##### Az CLI | ||
|
||
```sh | ||
az deployment group create --resource-group <resource-group> --template-file main.bicep | ||
``` | ||
|
||
##### Az Powershell | ||
|
||
```powershell | ||
New-AzResourceGroupDeployment -ResourceGroupName <resource-group> -TemplateFile .\main.bicep | ||
``` | ||
|
||
#### Deploy app and FIC resources only and forego end-to-end test | ||
|
||
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. | ||
|
||
##### Az CLI | ||
|
||
```sh | ||
az deployment group create --resource-group <YOUR-RESOURCE_GROUP> --template-file main.bicep --parameters graphRoles=[] | ||
``` | ||
|
||
##### Az Powershell | ||
|
||
```powershell | ||
New-AzResourceGroupDeployment -ResourceGroupName <resource-group> -TemplateFile .\main.bicep -graphRoles @() | ||
``` | ||
|
||
### Test calling Microsoft Graph with a secretless application | ||
|
||
>**NOTE**: This testing **assumes** that you have deployed all resources described in the template, as the test relies on using Azure Automation. | ||
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. | ||
|
||
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 | ||
the Bicep template deployment. | ||
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. | ||
3. On the runbook page, click on **Edit** and select **Edit in portal**. This brings up a page with an empty edit pane. | ||
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. | ||
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. | ||
|
||
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. | ||
|
||
[msi-as-fic]:https://learn.microsoft.com/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity?tabs=microsoft-entra-admin-center | ||
[priv-role-admin]:https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference#privileged-role-administrator | ||
[az-portal]:https://portal.azure.com | ||
[auto-accounts]:https://portal.azure.com/#view/HubsExtension/BrowseResource/resourceType/Microsoft.Automation%2FAutomationAccounts |
9 changes: 9 additions & 0 deletions
9
quickstart-templates/msi-as-a-fic-secretless/bicepconfig.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"experimentalFeaturesEnabled": { | ||
"extensibility": true | ||
}, | ||
// specify an alias for the version of the v1.0 dynamic types package you want to use | ||
"extensions": { | ||
"microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.1.8-preview" | ||
} | ||
} |
146 changes: 146 additions & 0 deletions
146
quickstart-templates/msi-as-a-fic-secretless/main.bicep
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
extension microsoftGraphV1 | ||
|
||
// TEMPLATE DESCRIPTION | ||
// Creates a secret-less client application, using a user-assigned managed identity | ||
// as the credential (configured as part of the application's federated identity credential). | ||
// The script optionally | ||
|
||
@description('Specifies the name of cloud environment to run this deployment in.') | ||
param cloudEnvironment string = environment().name | ||
|
||
// NOTE: Microsoft Graph Bicep file deployment is only supported in Public Cloud | ||
@description('Audience uris for public and national clouds') | ||
param audiences object = { | ||
AzureCloud: { | ||
uri: 'api://AzureADTokenExchange' | ||
} | ||
AzureUSGovernment: { | ||
uri: 'api://AzureADTokenExchangeUSGov' | ||
} | ||
USNat: { | ||
uri: 'api://AzureADTokenExchangeUSNat' | ||
} | ||
USSec: { | ||
uri: 'api://AzureADTokenExchangeUSSec' | ||
} | ||
AzureChinaCloud: { | ||
uri: 'api://AzureADTokenExchangeChina' | ||
} | ||
} | ||
|
||
@description('Specifies the resource group location.') | ||
param location string = resourceGroup().location | ||
|
||
@description('Specifies the user-assigned managed identity name to use as an application credential via federated identity credentials') | ||
param myWorkloadManagedIdentity string = 'myMSI-2024-12-18' | ||
|
||
@description('Specified the application display name') | ||
param applicationDisplayName string = 'myApp-2024-12-18' | ||
|
||
@description('Specifies the applications unique name identifier') | ||
param applicationName string = 'myApp-2024-12-18' | ||
|
||
@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.') | ||
param graphRoles array = ['Group.Read.All','Application.Read.All'] | ||
|
||
@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 []') | ||
param automationAccountName string = 'myAutomationAccount-2024-12-18' | ||
|
||
// login endpoint and tenant ID and issuer | ||
var loginEndpoint = environment().authentication.loginEndpoint | ||
var tenantId = tenant().tenantId | ||
var issuer = '${loginEndpoint}${tenantId}/v2.0' | ||
|
||
// create a user assigned managed identity scoped to a resource group | ||
resource myManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { | ||
name: myWorkloadManagedIdentity | ||
location: location | ||
} | ||
|
||
// Create a (client) application registration with a federated identity credential (FIC) | ||
// The FIC is configured with the managed identity as the subject | ||
// NOTE: app is configured with required properties only. Add the properties your app needs | ||
resource myApp 'Microsoft.Graph/[email protected]' = { | ||
displayName: applicationDisplayName | ||
uniqueName: applicationName | ||
|
||
resource myMsiFic '[email protected]' = { | ||
name: '${myApp.uniqueName}/msiAsFic' | ||
description: 'Trust the workload\'s user-assigned MI as a credential for the app' | ||
audiences: [ | ||
audiences[cloudEnvironment].uri | ||
] | ||
issuer: issuer | ||
subject: myManagedIdentity.properties.principalId | ||
} | ||
} | ||
|
||
// Create a service principal for the application | ||
resource mySP 'Microsoft.Graph/[email protected]' = { | ||
appId: myApp.appId | ||
} | ||
|
||
// NOTE: This section (to grant Microsoft Graph permissions) requires an elevated role | ||
// Grant the application only permission to Microsoft Graph | ||
// First find the Microsoft Graph service principal | ||
// Finally assign app roles to the app (if graphRoles is not an empty array) | ||
|
||
// 1. find Graph based on well-known appId | ||
resource msGraphSP 'Microsoft.Graph/[email protected]' existing = { | ||
appId: '00000003-0000-0000-c000-000000000000' | ||
} | ||
|
||
// 2. Grant the client app access to Microsoft Graph using the Oauth2 scope | ||
// which delegates the app to act as the sign-in user, constrained by the Oauth2 scope | ||
// This step only happens if the oauth2GraphScope is specified. | ||
|
||
// would use a for loop but that appears to be busted for some reason | ||
var graphAppRoles = msGraphSP.appRoles | ||
|
||
// Assign multiple app role assignments to MS Graph for the app/SP. | ||
// This gives the app/SP the necessary permissions to deploy this Bicep file (in app-only mode) | ||
resource appRoleAssignments 'Microsoft.Graph/[email protected]' = [for (role, i) in graphRoles: { | ||
appRoleId: filter(graphAppRoles, graphAppRoles => graphAppRoles.value == role)[0].id | ||
principalId: mySP.id // Client SP being granted permission to access the resource (API) | ||
resourceId: msGraphSP.id // Resource here is Microsoft Graph | ||
} | ||
] | ||
|
||
// Create an automation account and runbook to validate created application | ||
// can call Microsoft Graph without using a secret | ||
resource automationAccount 'Microsoft.Automation/automationAccounts@2023-11-01' = if(graphRoles != []) { | ||
name: automationAccountName | ||
identity: { | ||
type:'UserAssigned' | ||
userAssignedIdentities: { | ||
'${resourceGroup().id}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${myWorkloadManagedIdentity}':{} | ||
} | ||
} | ||
location: location | ||
properties: { | ||
sku: { | ||
name: 'Basic' | ||
} | ||
} | ||
resource myRunbook 'runbooks@2023-11-01' = { | ||
name: 'msi-as-fic-test-runbook' | ||
location: location | ||
properties: { | ||
description: 'Runbook for msi-as-fic testing using Az PowerShell' | ||
runbookType: 'PowerShell72' | ||
logProgress: false | ||
logVerbose: false | ||
} | ||
} | ||
} | ||
|
||
// outputs | ||
output clientAppId string = myApp.appId | ||
output ficIssuerAudience string = audiences[cloudEnvironment].uri | ||
output issuerURI string = issuer | ||
output tenantId string = tenantId | ||
output assignments array = [ for (role,i) in graphRoles: { | ||
appRoleIDName: appRoleAssignments[i].appRoleId | ||
}] | ||
output miPrincipalId string = myManagedIdentity.properties.principalId | ||
output miClientId string = myManagedIdentity.properties.clientId |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
using './main.bicep' | ||
|
||
param myWorkloadManagedIdentity = '[MANAGED-IDENTITY-NAME]' | ||
param applicationDisplayName = '[APPLICATION-DISPLAY-NAME]' | ||
param applicationName = '[APPLICATION-UNIQUE-NAME]' | ||
param cloudEnvironment = 'publicCloud' | ||
param graphRoles = ['Group.Read.All'] | ||
|
37 changes: 37 additions & 0 deletions
37
quickstart-templates/msi-as-a-fic-secretless/secretless-graph-request.ps1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
####################################################################### | ||
<# | ||
Perform a 2-legged flow to acquire an access token with a managed | ||
identity as the credential and then use it to call Microsoft Graph. | ||
PRE-REQUISITES: | ||
1. An app registered with a user-assigned managed identity as a | ||
federated identity credential (FIC). | ||
2. There's a service principal for the app. | ||
3. The service principal is is granted access to Microsoft Graph. | ||
SCRIPT STEPS | ||
1. Acquire a token for a user-assigned managed identity | ||
2. Run PS as an app, using the token from step 1 as the credential | ||
3. Call Microsoft Graph | ||
#> | ||
####################################################################### | ||
|
||
param | ||
( | ||
[Parameter(Mandatory=$true)] | ||
$managedIdentityPrincipalId, | ||
$applicationClientId, | ||
$tenantId | ||
$ficIssuerAudience | ||
) | ||
|
||
# Step 1: Acquire token for the managed identity | ||
Connect-AzAccount -Identity -AccountId $managedIdentityPrincipalId | ||
$token = Get-AzAccessToken -ResourceUrl $ficIssuerAudience | ||
|
||
|
||
# Step 2: Sign in to Azure PowerShell (as the app with the FIC configuration) | ||
Connect-AzAccount -ApplicationId $applicationClientId -FederatedToken $token.Token -Tenant $tenantId | ||
|
||
# Step 3: Get all Entra groups in the tenant (assumes app has Group.Read.All permission) | ||
Invoke-AzRestMethod -Method GET -Uri https://graph.microsoft.com/v1.0/groups |