-
Notifications
You must be signed in to change notification settings - Fork 12
First draft MSI as a FIC sample #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f8ec14d
First draft MSI as a FIC sample
dkershaw10 7dcfa97
Added README, a PS1 file and updated Bicep file
dkershaw10 0c31b9e
Minor changes to the readme
dkershaw10 c0bd526
Merge branch 'main' into dkershaw10-msi-as-fic-sample
dkershaw10 4e3d7b6
Update to allow for running in different clouds
dkershaw10 5a87b95
Minor update to the README
dkershaw10 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 | ||
| 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 hidden or 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 hidden or 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 hidden or 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'] | ||
|
|
36 changes: 36 additions & 0 deletions
36
quickstart-templates/msi-as-a-fic-secretless/secretless-graph-request.ps1
This file contains hidden or 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,36 @@ | ||
| ####################################################################### | ||
| <# | ||
| 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 | ||
| ) | ||
|
|
||
| # Step 1: Acquire token for the managed identity | ||
| Connect-AzAccount -Identity -AccountId $managedIdentityPrincipalId | ||
| $token = Get-AzAccessToken -ResourceUrl "api://AzureADTokenExchange" | ||
|
|
||
|
|
||
| # 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.