Azure
Active Directory (AAD): Microsoft's cloud solution for
managing users, applications, and more. Hosted on Microsoft's Azure
platform, this can integrate with on-premises Active Directory if
desired, but is not required.

(AAD) Tenant: An instance of Azure Active Directory
for a customer is called a "tenant". Most customers will have one
tenant, but larger organizations may have multiple for varying reasons.

A Service Principal?

In the Active Directory world, automated tasks are performed by service
accounts, be they traditional dedicated accounts or (group) managed
service accounts. The key to securely performing these tasks is ensuring
that unique security principals are used for each task, ensuring they have
only the amount of access they need to perform the task in question, and
that they're monitored proactively.

Azure Active Directory does not have the concept of service accounts, but
there is a functional equivalent: Service
Principals. Service principals are comprised of:

Azure Active Directory Registered Application: Registering
an application in AAD is a way to then grant permissions (using a
Service Principal) to that application within Azure and/or Azure Active
Directory. This can be an application hosted in Azure, externally, or in
our case, an automation task of another nature. An AAD registered
application can also be used by other tenants (with a Service Principal
on their side), but as we're talking about automating our own tasks that
is outside the scope of this article.

The Service Principal itself: The service principal
is an association with an AAD Application that allows for granting of
permissions within that tenant. In our case we'll be discussing AAD
registered apps and service principals in a 1:1 ratio.

Azure Entitlement Domains

Subscription: One can grant a service principal,
user, or other object access at an entire subscription level. There are
very few cases where doing so would adhere to the principle of least
privilege, so don't do this unless you have no option.

Resource Group: A resource group is a logical
grouping of Azure resources. Depending on how you split your resource
groups, this is likely a common place to assign privileges.

Object: Privileges can be assigned on a per-object
basis as well, but managing security on a per-object basis is very
complex and usually only done when absolutely necessary from a security
perspective.

Sorry son, your
RBAC doesn't give you access to the keyvault.

Usage Examples

So when would we use a service principal? Here are a couple examples:

Webjobs that interact with other resources: If you
have webjobs
that interact with other resources in your subscription you may choose
to use a service principal to access those resources. An excellent
example of this is the Let's
Encrypt! web app extension.

Automated Tasks: Azure automation and/or external
jobs running against Azure can leverage service principals to
authenticate and perform their work. Code deployment platforms such as
OctopusDeploy and VSTS are perfect examples.

Let Us Do This

And by this I mean the point of the article.

To make quick work of this from an automation perspective, we'll make a
quick PowerShell function we can re-use in other scripts. The main
PowerShell cmdlets we'll be leveraging are:

Just running these two commands is easy enough, but that's not all that
useful from an automation perspective where we want to automate multiple
operations that rely on the creation or existence of a service principal.
To that end, we'll make a function that can accept a desired service
principal name, check to see if it exists & create it if not. This
function will output an object with all the necessary information for
further use. We'll also generate a password if one isn't specified and
report status regarding if the service principal already existed or
not.We'll return everything in an object so our scripts can take
appropriate actions for all possible scenarios.

Critical Note: The code below contains reference to a
function that is not included, so before copying/pasting please read at
least the "Note" sections in the code discussion below.

Update 1/2018: AzureRM 5.0 cmdlets require a securestring
for New-AzureRmADApplication whereas it was not supported previously.

################################################################################
# Register-AzureServicePrincipal
# Given the correct input, does one of the following:
# 1> checks for existence of application registration
# 2> checks to see if the app is registered as a service principal
# 3> if neither of those is true, creates the app and service principal
# > outputs an object with all details possible. If the app already exists the
# password will be null because we can't look it up.
# INPUT: servicePrincipalName, the displayname of the desired App/ServicePrincipal and the desired password.
# The password field is optional and if omitted a 30 character random password will be generated and returned.
# OUTPUT: an object containing the following NoteProperties
# > ClientID: the GUID representing the application ID
# > ServicePrincipalID: the GUID representing the Service Principal association
# > SPNNames: The service principal names of the SP
# > ServicePrincipalPassword: A securestring of the Application Password. NOTE: This will be NULL if the app is already registered in AD as we cannot retrieve it.
# > ServicePrincipalAlreadyExists: boolean to indicate if the sp already existed or not
# USAGE NOTES: Assumes already logged into Azure with proper permissions and that the desired subscription is selected.
Function Register-AzureServicePrincipal{
param(
# The name for the service principal. We won't make this mandatory to allow for manual entry mode with guidance. Obviously it needs to be specified for automation.
[string]$servicePrincipalName,
# the password if you choose to specify it, otherwise the script will generate one for you.
[string]$servicePrincipalPassword
)
# Set the regex for the input validation on the SPN
$SPNNamingStandard='^[--z]{5,40}$'
Write-Host "Provisioning AzureAD App/Service Principal"
Write-Warning "The account operating this script MUST have the role Subscription Admin or Owner in the desired subscription"
$ErrorActionPreference = "Stop" # Error handing is not yet sufficient; try/catch the stuff below!
if (!$servicePrincipalName){
do {
Write-Host "SPN naming standard is (in RegEx): $SPNNamingStandard"
$servicePrincipalName=Read-Host "Service Principal Name not specified on startup; Please enter desired name or type GUID and press enter for a guid based random name"
if ($servicePrincipalName -eq "GUID"){
$guid=([guid]::NewGuid()).toString()
$servicePrincipalName="SPN-$guid"
}
} until ($servicePrincipalName -match $SPNNamingStandard)
}
# handle command line specification of GUID
if ($servicePrincipalName -eq "GUID"){
$guid=([guid]::NewGuid()).toString()
$servicePrincipalName="SPN-$guid"
}
# set URL and IdentifierUris
$homePage = "http://" + $servicePrincipalName
$identifierUri = $homePage
Write-Host "Desired Service Principal Name is $servicePrincipalName `n"
# Now we need to determine if 1> the Application exists and 2> if it has been registered as a service principal. This will guide our execution through the end of the function.
$appExists=Get-AzureRmADApplication -DisplayNameStartWith $servicePrincipalName -ErrorAction SilentlyContinue
# check for SPN only if app exists. SPN can't exist without app so no reason to check if not.
if ($appExists){$spnExists=Get-AzureRmADServicePrincipal | Where-Object {$_.ApplicationId -eq $appExists.ApplicationId} -ErrorAction SilentlyContinue}
# we only need a password if the app hasn't been created yet.
if (!$appExists){
# Generate a password if needed
if (!$servicePrincipalPassword){
$servicePrincipalPassword=New-RandomPassword -passwordLength 40
}
# NOTE! We had a convertto-securestring here but as it turns out new-azurermadapplication doesn't take a securestring, only a string
# NOTEUPDATE! AzureRM 5.0 and higher requires a securestring (yay!) This has been updated but notes left here for reference.
$servicePrincipalPassword=ConvertTo-SecureString $servicePrincipalPassword -AsPlainText -Force
}
# we set this to NULL as a "valid" return as the appID already exists and we can't lookup the password from here
else {$servicePrincipalPassword=$null}
# Create the App if it wasn't already
if (!$appExists){
$azureADApplication=New-AzureRmADApplication -DisplayName $servicePrincipalName -HomePage $homePage -IdentifierUris $identifierUri -Password $servicePrincipalPassword
Write-Host "Azure AAD Application creation completed successfully"
}
# if it already exists we'll just redirect the variable
else{$azureADApplication=$appExists}
$appID=$azureADApplication.ApplicationId
# Create new SPN if needed
if (!$spnExists){
$spn=New-AzureRmADServicePrincipal -ApplicationId $appId
Write-Host "SPN creation completed successfully"
}
else{$spn=$spnExists}
$spnNames=$spn.ServicePrincipalNames
# Create object to store information.
$outputObject=New-Object -TypeName PSObject
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalName -Value $servicePrincipalName
$outputObject | Add-Member -MemberType NoteProperty -Name ClientID -Value $appID
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalID -Value $spn.Id
$outputObject | Add-Member -MemberType NoteProperty -Name SPNNames -Value $spnNames
$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalPassword -Value $servicePrincipalPassword
if ($appExists -and $spnexists){$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalAlreadyExists -Value $true}
else {$outputObject | Add-Member -MemberType NoteProperty -Name ServicePrincipalAlreadyExists -Value $false}
return $outputObject
}
################################################################################

Discussion

param(
# The name for the service principal.
[string]$servicePrincipalName,
# the password if you choose to specify it, otherwise the script will generate one for you.
[string]$servicePrincipalPassword
)

This is where the input to the function is defined; as you'll see below
the password is optional, but the servicePrincipalName is mandatory. I
don't mark the parameter as mandatory in the parameter definition to
facilitate interactive use of this script. Feel free to change add mandatory=$true
if desired.

While it would be logical to use [securestring] for the
servicePrincipalPassword, the PowerShell cmdlet we're going to use
downstream only supports regular strings at this time.

A quick note on interactive vs. non-interactive scripts

While the goal of automation should be running tasks headless, thus fully
non-interactive, there are some scenarios where facilitating both
non-interactive and interactive running can be very useful. In some cases
when acting as a consultant I will allow for full non-interactive running
of automation scripts so the customer can walk through each option guided
once to understand the context of the options available to them. At the
end of script execution, I echo out what the equivalent command line would
be to the console if it were run entirely headless. This scenario does
require a bit more control flow logic, mainly using an additional
parameter to specify we're in a headless scenario and error out quickly
prior to execution when running in those scenarios. This also allows for
use of code snippets (mainly functions) on a day-to-day basis as well as
in dedicated automation framework.

# Set the regex for the input validation on the SPN
$SPNNamingStandard='^[--z]{5,40}$'
Write-Host "Provisioning AzureAD App/Service Principal"
Write-Warning "The account operating this script MUST have the role Subscription Admin or Owner in the desired subscription"
$ErrorActionPreference = "Stop" # Error handing is not yet sufficient, try/catch the stuff below!
if (!$servicePrincipalName){
do {
Write-Host ""
Write-Host "SPN naming standard is (in RegEx): $SPNNamingStandard"
$servicePrincipalName=Read-Host "Service Principal Name not specified on startup; Please enter desired name or type GUID and press enter for a guid based random name"
if ($servicePrincipalName -eq "GUID"){
$guid=([guid]::NewGuid()).toString()
$servicePrincipalName="SPN-$guid"
}
} until ($servicePrincipalName -match $SPNNamingStandard)
}

$SPNNamingStandard and the following logic is if you want to use a
RegEx to ensure interactive input is validated. Change this expression to
meet your needs or return the entire section if you don't want to facilitate
interactive running. Limited time bonus offer, here's
a link to my favorite regular expression evaluation site!

Note: As the script warns, the credential used to create
the Application ID/Service Principal must have the role Subscription Admin
or Owner to perform the provisioning actions.

This little trick allows the specification of "GUID" (i.e. Register-AzureServicePrincipal
-$servicePrincipalName GUID) to tell our function to generate a GUID
for the name. At the end you see I'm pre-pending a "SPN-" to the GUID to
ensure the programmatically generated ApplicationIDs/SPNs stand out.

Homepage and IdentifierURI settings for a service principal that we're
using in the aforementioned capacity don't matter, but they do need to be
set, so we base them on the SP name itself an move on.

Update 1/2018: AzureRM 5.0 cmdlets require a securestring
for New-AzureRmADApplication whereas it was not supported previously.

# Now we need to determine if 1> the Application exisists and 2> if it has been registered as a service principal. This will guide our execution through the end of the function.
$appExists=Get-AzureRmADApplication -DisplayNameStartWith $servicePrincipalName -ErrorAction SilentlyContinue
# check for SPN only if app exists. SPN can't exist without app so no reason to check if not.
if ($appExists){$spnExists=Get-AzureRmADServicePrincipal | Where-Object {$_.ApplicationId -eq $appExists.ApplicationId} -ErrorAction SilentlyContinue}
# we only need a password if the app hasn't been created yet.
if (!$appExists){
# Generate a password if needed
if (!$servicePrincipalPassword){
$servicePrincipalPassword=New-RandomPassword -passwordLength 40
}
# NOTE! We had a convertto-securestring here but as it turns out new-azurermadapplication doesn't take a securestring, only a string
# NOTEUPDATE! AzureRM 5.0 and higher requires a securestring (yay!) This has been updated but notes left here for reference.
$servicePrincipalPassword=ConvertTo-SecureString $servicePrincipalPassword -AsPlainText -Force
}
# we set this to NULL as a "valid" return as the appID already exists and we can't lookup the password from here
else {$servicePrincipalPassword=$null}

This code allows us to insert this function into a workstream
regardless of if the Application ID and service principal already exist. If
they do, we get all the information we can but set the password to $null
since we can't look it up. As you'll see below we also add another
noteproperty to inform the caller explicitly that the AppID/service
principal already existed.

Note: This script block contains reference to another
function that I have not provided, New-RandomPassword. You'll need
to provide your own function that generates a password and call it here or
specify the desired password when calling the function explicitly. Perhaps
I'll write another post in the future to cover generating a random password
in PowerShell.

Now we do the actual creation of the Application ID and service
principal if necessary. Notice that if they already existed we set the
downstream variables to relay to the user. This section could be improved
with additional error handling if desired.

$ouput.ClientID=The client ID (appID) is one of the critical pieces of
info for downstream applications. This is what you'll specify when
authenticating later (think of it as your user ID).

$output.ServicePrincipalID=The SP ID, though not used in any capacity
directly that I've seen yet short of programmatically referencing it
when deleting, etc.

$output.SPNNames=The reference names of the service principal. These
would be used by third party apps, but in most cases I'm addressing with
this article they'll go unused.

$output.ServicePrincipalPassword=Keep it secret! Keep it safe! This is
a clear text copy of the password associated with this service
principal. Obviously this is the other key piece of information you'll
need as a takeaway. The prudent next step would be to check this into a
Azure Keyvault or something similar, but that's for another article...

$output.ServicePrincipalAlreadyExists=$true or $false,
this is also critical for downstream processing. If $true, you'll know
that this is newly created and the password is contained in the object
meaning you'll need to scrape and store/use that accordingly. If $false
it means you can look up the ID by the name if needed, but you better
have the password stored somewhere else as we can't look it up now.
Either way you have two clear courses of action. While we could have
relied on the password being $null, I added this property to
definitively set it one way or the other to account for any unknown
circumstances due to upstream changes down the road.

Note: Make sure you both store the password for the
newly created service principal as you won't be able to retrieve
it in the future. Also, make sure your session or variables are
cleared after running as the password exists in clear text in
memory.

Update/Note 2: By default, this password is only good for
one year, and will expire after that time making it impossible
to use the SPN. To manage the password on an existing object, you'll need
to use the Get/Remove/New-AzureAdApplicationPasswordCredential cmdlets
in the AzureAD module (not AzureRM).

Bringing it Home

Now that we've created the function, let's use it to create a principal and
give it access to a resource group:

This will use our function to create a service principal and give
it contributor level access to <myResourceGroup>. If you have multiple
environments in your subscription you should create a principal for each and
restrict access to resource group(s) associated with each environment. I
also recommend splitting production into a separate subscription; the entire
reasons behind that are a story for another article...

Did it Work?

If you would like to manually check your work, you can find it in the Azure
portal by navigating to the hamburger menu -> "More Services" ->
"Azure Active Directory" -> "App Registrations".

There you should see your newly created app.

You can also check the role based access controls on your resource group or
whatever object you applied the permissions to.

Only the chosen
shall access Nachos Deathstar.

In Conclusion

The Azure model of auth/auth management is sound, but adherence to long
standing security design principles requires a bit of effort. Hopefully this
article will assist you in doing so. Please leave any
comments/criticism/coffee donations below.