Ask Learn
Preview
Ask Learn is an AI assistant that can answer questions, clarify concepts, and define terms using trusted Microsoft documentation.
Please sign in to use Ask Learn.
Sign inThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Hi,
Recently, I wrote a short blog post on how to provision Azure Active Directory (AAD) Apps in a highly controlled way, so I will not repeat all I said there, but it a nutshell, the idea is to make sure DevOps can automate the creation/update/deletion of AAD Apps entirely from VSTS while not being able to interact with non-DevOps apps.
Here is a step by step process on how to get there. Note that almost everything could be done from VSTS but, often, in organizations, the below tasks will involve different people & even different teams, hence the reason I decouple all the tasks.
As a Global Admin, go to the Azure Portal (I don't use PowerShell yet for sake of simplicity):
https://login.microsoftonline.com/tenantid/oauth2/authorize?client\_id=yourappid\&response\_type=code\&resource=https://graph.windows.net\&prompt=admin\_consent
You will get prompted to consent, so just accept it!
Now that we have created the VSTS app, we need to grant it access to the subscription via Role Based Access Control (RBAC). To do so, go to the Subscriptions link (bottom of the page in the portal) ==> Access Control ==> Add, you should get a screen similar to this:
You have to select the Role from the dropdown. Here, there is no single answer to which role is the most suitable as it will depends on the enterprise you're working for. In some, DevOps would receive dedicated subscription(s), in others, DevOps will share subscriptions with other stakeholders, so of course, the role you're choosing, heavily depends on this. As you may notice, for the time being, there is nothing for AAD, hence the trick of granting permissions using OAuth's application permissions instead of RBAC.
In the Select textbox, just enter the App Id of the App we've just created and save your changes. Your VSTS app is now granted the Contributor/Owner role to the target subscription.
Now, it's time to create the VSTS endpoint.
Ok, go grab a coffee, it's time to summarize what we've done so far:
We're now ready to start building the thing in VSTS.
In order to gain access to AAD from our PowerShell scripts, we'll have to grab an access token using the VSTS app's credentials. That's where this differs from RBAC. With RBAC, the PowerShell scripts executed by VSTS will automatically use the role you granted them, not with AAD since there is no role as we've seen earlier. So, in order to store the app credentials in a safe place, we'll create a variable group.
Alternatively, if you happen to have on-premises build servers, you could also define environment variables at the host level. This comes with the advantage that you won't need to link the variable group to the release definition later on but the counterpart is that you must think of adding these environment variables whenever you add a new build server. Up to you to decide what's best for you!
So, I assume you're using the online agents and here is how to create the variable group:
Make sure to click on the small lock at the bottom right to hide the value.
So, if you're familiar with VSTS, you might want to build a custom task that you can reuse across release definitions as represented by the below screenshot.
Since building a custom task implies the setup of NPM and TFS CLI, I will not take that path, it'd be too long and I don't want to force you drinking two extra coffees to be able to keep reading this article :). However, if you want to know more about it, you can go to this repo https://github.com/Microsoft/tfs-cli
Whatever option you decide (custom task or not), you'll have to bind it to a release definition. So, I'm going to take some shortcuts as I will directly write a PowerShell script inside of the release definition so that you get the idea. In the real world, you'd rather have a build definition generating some artefacts such as a JSON file describing the Apps to be deployed by a solution and you'd bind this JSON file to your custom VSTS task in a similar way that what you do with ARM templates. But here again, I want to focus on the essence of this article so that it's not too lengthy, I leave the rest to your imagination.
Ok, that said:
Install-Module -Name AzureADPreview -Force -Scope CurrentUser
$body="resource=https%3A%2F%2Fgraph.windows.net&client_id=vstsappclientid&client_secret=$(vstsappsecret)&grant_type=client_credentials"
$resp=Invoke-WebRequest -UseBasicParsing -Uri https://login.microsoftonline.com/yourtenant/oauth2/token -Method POST -Body $body| ConvertFrom-Json
write-host "token is $resp.access_token" Connect-AzureAD -TenantId yourtenant -AadAccessToken $resp.access_token -AccountId user@yourtenant
New-AzureADApplication -DisplayName "testapp" -PublicClient $true
Some values must be replaced by your own values. Here is what is important from the above snippet:
The access token you get should contain the following role (minimum):
Remember that the access token is valid for 1 hour which should be largely enough for the execution of your PowerShell script but keep that in mind should your script run longer. Of course, you can elaborate a little bit. Imagine that this script takes the following JSON as input:
and if, on top of the above lines of code, you'd add something like this:
$apps = Get-Content apps.json |ConvertFrom-Json
foreach($app in $apps.applications)
{
$TargetApp = Get-AzureADApplication -Filter "DisplayName eq '$($app.name)'"
if($TargetApp -eq $null)
{
if(!$app.IsPublicClient)
{
$NewApp = New-AzureADApplication -DisplayName "$($app.name)" -IdentifierUris $app.IdentifierUri
New-AzureADServicePrincipal -AppId $NewApp.AppId
}
else{
$NewApp = New-AzureADApplication -DisplayName "$($app.name)" -PublicClient $true
}
foreach($access in $app.RequiredResourceAccess)
{
$TargetResource = (Get-AzureADServicePrincipal | where {$_.ServicePrincipalNames.Contains($access.resource)})
if($TargetResource -ne $null)
{
$ResourceAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{
ResourceAppId=$TargetResource[0].AppId ;ResourceAccess=@{}}
foreach($perm in $access.perms)
{
$PermAccess = $TargetResource[0].Oauth2Permissions | ? {$_.Value -eq "$($perm.name)"}
$res=[Microsoft.Open.AzureAD.Model.ResourceAccess]@{
Id = $PermAccess.Id ;
Type = "Scope"}
$ResourceAccess.ResourceAccess.Add($res);
}
Set-AzureADApplication -ObjectId $NewApp.ObjectId -RequiredResourceAccess $ResourceAccess
}
else
{
write-host "Resource $($access.resource) not found" -Verbose
}
}
}
else{
write-host "App $($app.name) already exists";
}
}
you'd provision your new apps according to what is defined in the JSON file which in this case is an AAD App for a webapi and a native app that is granted access to that API. So, for sure, this script should be reworked (take into account app-only permissions, take into account updates, add error handling, etc...) but I hope you got the point!
If you combine this approach with the recently MSI announcement, you have everything in hands to store your app secrets (there is none in the above example) into Key Vault and let your web apps accessing it safely without having to disclose any information. I'll write a separate article on MSI very soon.
Happy DevOps
Ask Learn is an AI assistant that can answer questions, clarify concepts, and define terms using trusted Microsoft documentation.
Please sign in to use Ask Learn.
Sign in