B2B Access with Azure AD Access Packages

With the advent of modern collaboration platforms, users are no longer content to work within the organisational boundary. More and more organisations are being challenged to bring in external partners and users for projects and day to day operations. But how can we do this securely? How do IT managers minimise licensing costs? Most importantly, how can we empower the business to engage without IT? This problem is at the forefront of the thinking behind Azure AD Access Packages. An Azure solution enabling self service onboarding of partners, providers and collaborators at scale. Even better than that, this solution enables both internal and external onboarding. You can and should set this up internally, the less work that IT has to do managing access, the better right?

Before we dig too deep, I think a brief overview of how access packages are structured would be useful. On a hierarchy level, packages are placed into catalogs, which can be used to enable multiple packages for a team to use. Each package holds resources, and a policy defines the who and when of requesting access to these using the process. The below diagram from Microsoft neatly sums this up.

Access Package Hierarchy

This all sounds great I hear you saying. So what does this look like? If you have an Office 365 account, you’re welcome to log in and look for yourself here, otherwise a screenshot will have to do.

External Access Package UI

To get started with this solution, you will need an Azure AD P2 licensed tenant. Most organisations will obtain P2 licences this through an M365 E5 subscription, however you can purchase these directly if have M365 E3 or lower and are looking to avoid some costs. You will need to have at-least a 1:1 license assignment for internal use cases, while external identity has recently moved to a “Monthly Active Users” licensing model. One P2 licence in your tenant will license the first 50 thousand external users for free!

Once you’ve enabled this, head on over to the “Identity Governance” blade within Azure AD. This area has a wealth of functionality that benefits nearly all organisations, so I would highly recommend investigating the other items available here. Select Access Packages to get started.

The UI itself for creating an access packages is quite simple, clicking create-new will walk you through a process of assigning applications, groups, teams & share-point sites.

Access Package creation UI

Unfortunately some services like Windows Virtual Desktop will not work with access packages, however this is a service limitation rather than an Azure AD limitation. Expect these challenges to be resolved over time.

At the time of writing, the AzureADPreview module does not support Access Packages. Microsoft Graph beta does however, and so, have an MS Graph based script!

# Fastest way to get one of these is to export your token from Graph Explorer. 
# Using a custom application registration requires Graph API permission approval & scopes request using an ROPC flow
$graphToken = "SuperLongBase64EncodedJWT-BearerToken"

#Access Package Details
$accessPackageName = "my-access-package"
$accessPackageDescription = "This package contains resources for B2B"
$resourceCatalogName = "General" #This can be updated to use other catalogues. This script does not create catalogs.

#Array of access Access package resources - This example only an AAD Group and the Azure Devops Enterprise app. 
$catalogResourceList = @(@{
        'displayName' = 'Testing-Group' #Must match the name of the object your are specifying
        'description' = 'Group for managers to become a member of' #not used as the description of the object, used as the description of the request
        'resourceType' = 'AadGroup' 	#The type of the resource, such as Application if it is an Azure AD connected application, or SharePoint Online Site for a SharePoint Online site.
        'originId' = '3c6aded3-af34-4bac-93fe-b906de09b6c9' # 	The unique identifier of the resource in the origin system. In the case of an Azure AD group, this is the object id of the group.
        'originSystem' = 'AadGroup' #Can be SharePointOnline, AadApplication or AadGroup.
        'roleDisplayName' = 'Member' #Access Packages require roles to be assigned to a user. This is the displayname for the role as it appears within Azure AD metadata (for apps) or Member/Owner for groups. If developing programatically, easy to add a AP & pull from that area
        'displayName' = 'Azure DevOps' 
        'description' = 'Group for people using Azure DevOps'
        'resourceType' = 'Application'
        'originId' = '03004e60-c002-43d3-a63d-36f1f4707e70' 
        'originSystem' = 'AadApplication'
        'roleDisplayName' = 'Default Access' 

#Build headers for graph API requests
$authorisationHeaders = @{ 
    'Authorization' = "Bearer $graphToken"
$accessPackageHeaders = @{
    'Authorization' = "Bearer $graphToken"
    'Content-Type' = "application/json"

Write-Host "Retrieving resource ID for the $resourceCatalogName catalog." -ForegroundColor Green
#Get Resource Catalog
$allCatalogResult = Invoke-RestMethod -Uri '' -Headers $authorisationHeaders
$defaultCatalogId = ($allCatalogResult.Value | Where-Object{$_.displayName -eq "$resourceCatalogName"}).id

Write-Host "Creating Access Package: $accessPackageName" -ForegroundColor Green
#Create Access Package
$accessPackageBody = @{
    'catalogId' = $defaultCatalogId
    'displayName' = $accessPackageName
    'description' = $accessPackageDescription
$accessPackageParams = @{
    'Uri'         = ""
    'Method'      = 'Post'
    'Headers'     = $accessPackageHeaders
    'Body'        = ($accessPackageBody | ConvertTo-Json)
$accessPackageResult = Invoke-RestMethod @accessPackageParams

#Create Resources within the catalog & assign each to the newly created access package
$catalogResourceList | ForEach-Object{
    Write-Host "Creating the $($_.displayName) resource object for consumption within an access package." -ForegroundColor Green
    $catalogResourceBody = "{
        'catalogId' : '$defaultCatalogId',
        'requestType': 'AdminAdd',
        'justification': 'Provisioning as part of access Package creation',
        'accessPackageResource': {
            'displayName': '$($_.displayName)',
            'description': '$($_.description)',
            'resourceType': '$($_.resourceType)',
            'originId': '$($_.originId)',
            'originSystem': '$($_.originSystem)'
    $catalogResourceParams = @{
        'Uri'         = ""
        'Method'      = 'Post'
        'Headers'     = $accessPackageHeaders
        'Body'        = $catalogResourceBody
    $catalogResourceResult = Invoke-RestMethod @catalogResourceParams

    #Get the ID of the resource as represented within access packages
    $catalogResource = (Invoke-RestMethod -Uri "$defaultCatalogId/accessPackageResources?`$filter=(displayName eq '$($_.displayName)')" -Headers $authorisationHeaders).value
    #Get the ID of the specified role as defined in the original objects
    $catalogResourceRole = (Invoke-RestMethod -uri "$defaultCatalogId/accessPackageResourceRoles?`$filter=(originSystem+eq+%27$($_.originSystem)%27+and+accessPackageResource/id+eq+%27$($$($_.roleDisplayName)%27)&`$expand=accessPackageResource" -Headers $authorisationHeaders).value
    #Assign the Resource & Associated Role to the Access Package
    Write-Host "Assigning resource role $($catalogResourceRole.displayName) for $($_.displayName) to Access Package $($" -ForegroundColor Green
    $roleAssignmentBody = "{
        'accessPackageResourceRole': {
          'accessPackageResource': {
        'accessPackageResourceScope': {
    $roleAssignmentParams = @{
        'Uri'         = "$($"
        'Method'      = 'Post'
        'Headers'     = $accessPackageHeaders
        'Body'        = $roleAssignmentBody
    $roleAssignmentResult = Invoke-RestMethod @roleAssignmentParams

#Create an admin assignement policy for the access package
Write-Host "Creating an admin assignment policy for access package id: $($" -ForegroundColor Green
$defaultAccessPackagePolicyBody = "{
    '@odata.context': '`$metadata#accessPackageAssignmentPolicies/`$entity',
    'accessPackageId': '$($',
    'displayName': 'Default Access Policy - Admin Assignment',
    'description': 'Users with access to this package are assigned by an admin',
    'canExtend': false,
    'durationInDays': 365,
    'expirationDateTime': null,
    'accessReviewSettings': null,
    'requestorSettings': {
        'scopeType': 'NoSubjects',
        'acceptRequests': true,
        'allowedRequestors': []
    'requestApprovalSettings': {
        'isApprovalRequired': false,
        'isApprovalRequiredForExtension': false,
        'isRequestorJustificationRequired': false,
        'approvalMode': 'NoApproval',
        'approvalStages': []
    'questions': []
$defaultAccessPackagePolicyParams = @{
    'Uri'         = ""
    'Method'      = 'Post'
    'Headers'     = $accessPackageHeaders
    'Body'        = $defaultAccessPackagePolicyBody

$defaultAccessPackagePolicyResult = Invoke-RestMethod @defaultAccessPackagePolicyParams

While all this PowerShell might look a bit daunting, understand all that is being done is generating API request bodies and pushing that over 6 basic API calls;

  1. Retrieve information about our specified catalog (General)
  2. Create an Access Package
  3. Add each resource to our specified catalog
  4. Get each resource’s available roles
  5. Assign the resource & role to our Access Packages
  6. Create a Policy which enables assignment of our Access Package

Hopefully this article has provided you with a decent overview of Azure AD Access Packages. There are a lot of benefits when applying this in B2B scenarios, especially when it comes to automating user onboarding & access management. With big investments & changes from Microsoft occurring in this space, expect further growth & new features as the year comes to a close!

Please Note: While we do distribute an access package link within this blog, requests for access are not sent to an monitored email and will not be approved. If you would like to know more, please don’t hesitate reach out as we would be happy to help.

[mailpoet_form id="1"]

Other Recent Blogs

Level 9, 360 Collins Street, 
Melbourne VIC 3000

Level 2, 24 Campbell St,
Sydney NSW 2000

200 Adelaide St,
Brisbane QLD 4000

191 St Georges Terrace
Perth WA 6000

Level 10, 41 Shortland Street

Part of

Arinco trades as Arinco (VIC) Pty Ltd and Arinco (NSW) Pty Ltd. © 2023 All Rights Reserved Arinco™ | Privacy Policy
Arinco acknowledges the Traditional Owners of the land on which our offices are situated, and pay our respects to their Elders past, present and emerging.

Get started on the right path to cloud success today. Our Crew are standing by to answer your questions and get you up and running.