Skip to content

Instantly share code, notes, and snippets.

@merill
Last active March 29, 2022 23:01
Show Gist options
  • Save merill/70fe6e278fb36c1a142bdbe3175cbdb6 to your computer and use it in GitHub Desktop.
Save merill/70fe6e278fb36c1a142bdbe3175cbdb6 to your computer and use it in GitHub Desktop.

Extended Domain Support for Azure AD B2B

This page provides a workaround for organizations that are approaching the limit on the Allow/Block Domains for guest invites. The domain list is currently limited to 25kb.

Solution Overview

The high level overview for this solution involves

  • Creating a new extension attribute (eg 'approvedForCollab')
  • Create a Conditional Access policy that blocks all guests that don't have 'approvedForCollab' = Yes
  • Create a Logic app that runs every 5/10 minutes. This logic app will
    • Check for new guests invited to the tenant (querying /users)
    • Verify if the user's domain/tenant Id matches a known list (or is included in Connected orgs)
    • Updates the approvedForCollab to Yes/No
  • Disable the out of the box Allow/Deny option

Benefits of this solution

  • No limit on the list of allowed domains
  • Basic solution in the Logic app can be extended to suite business requirements, eg
    • Look up Allow/Deny from a SharePoint list
    • SharePoint list can have custom approval process to add new domains to the list
    • Include meta data on business owner that requested a domain etc
  • Support for adding a single Tenant ID and including all verified domains associated with the TenantId
  • Logic app includes support for looking up connected orgs
  • IT Admins can update 'approvedForCollab' user attribute to allow one-off users instead of allowing the entire domain.

Setup Guide

Set up Azure Logic App

  • In Azure create Logic App
  • In the Identity blade enable System Assigned Managed Identity (Use this Service Principal ID in the script below)
  • Switch to Code view and copy/paste the attached .json file
  • Scehdule the Logic app to run every 10 minutes

Assign Permissions to Managed Identity

  • Grant Application permissions to the managed identity
    Connect-AzureAD
    
    $spID = '680210cc-fa88-4b4f-89f9-d3d7aab98cb4 TODO Replace with your Managed Identity ID' # Managed Identity ID for the Logic App's system account
    # Check the Microsoft Graph documentation for the permission you need for the operation. 
    $permissions = @('User.ReadWrite.All', 'AuditLog.Read.All', 'EntitlementManagement.Read.All')
    # Get the service principal for Microsoft Graph. 
    # First result should be AppId 00000003-0000-0000-c000-000000000000
    $GraphServicePrincipal = Get-AzureADServicePrincipal -SearchString "Microsoft Graph" | Select-Object -first 1 
    foreach($perm in $permissions){
        # Assign permissions to the managed identity service principal. 
        $AppRole = $GraphServicePrincipal.AppRoles | Where-Object {$_.Value -eq $perm -and $_.AllowedMemberTypes -contains "Application"}
        New-AzureAdServiceAppRoleAssignment -ObjectId $spID -PrincipalId $spID -ResourceId $GraphServicePrincipal.ObjectId -Id $AppRole.Id
    }  

Create Extension Attribute

  • Create Extension Attribute to store guest status
    $MyApp = (New-AzureADApplication -DisplayName "GuestAccessUserAttributeApp" -IdentifierUris "https://GuestAccessUserAttributeApp").ObjectId
    New-AzureADServicePrincipal -AppId (Get-AzureADApplication -SearchString "GuestAccessUserAttributeApp").AppId
    New-AzureADApplicationExtensionProperty -ObjectId $MyApp -Name "approvedForCollab" -DataType "String" -TargetObjects "User"

Create Conditional Access Policy

  • Create a conditional access policy that blocks guest users that don't have the 'approvedForCollab' extension attribute set to Yes
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Get_Connected_Orgs": {
"actions": {
"For_each": {
"actions": {
"For_each_2": {
"actions": {
"Condition_2": {
"actions": {
"Append_to_array_variable_2": {
"inputs": {
"name": "AllowedExternalUserDomains",
"value": "@items('For_each_2')?['tenantId']"
},
"runAfter": {},
"type": "AppendToArrayVariable"
}
},
"else": {
"actions": {
"Append_to_array_variable": {
"inputs": {
"name": "AllowedExternalUserDomains",
"value": "@items('For_each_2')?['domainName']"
},
"runAfter": {},
"type": "AppendToArrayVariable"
}
}
},
"expression": {
"and": [
{
"equals": [
"@empty(item()?['domainName'])",
"@true"
]
}
]
},
"runAfter": {},
"type": "If"
}
},
"foreach": "@items('For_each')['identitySources']",
"runAfter": {},
"type": "Foreach"
}
},
"foreach": "@body('Parse_ConnectedOrgs_JSON')?['value']",
"runAfter": {
"Parse_ConnectedOrgs_JSON": [
"Succeeded"
]
},
"type": "Foreach"
},
"Get_connectedOrganizations": {
"inputs": {
"authentication": {
"audience": "https://graph.microsoft.com",
"type": "ManagedServiceIdentity"
},
"method": "GET",
"uri": "https://graph.microsoft.com/beta/identityGovernance/entitlementManagement/connectedOrganizations"
},
"runAfter": {},
"type": "Http"
},
"Parse_ConnectedOrgs_JSON": {
"inputs": {
"content": "@body('Get_connectedOrganizations')",
"schema": {
"properties": {
"@@odata.context": {
"type": "string"
},
"value": {
"items": {
"properties": {
"createdBy": {
"type": "string"
},
"createdDateTime": {
"type": "string"
},
"description": {
"type": "string"
},
"displayName": {
"type": "string"
},
"id": {
"type": "string"
},
"identitySources": {
"items": {
"properties": {
"displayName": {
"type": "string"
},
"domainName": {
"type": "string"
},
"tenantId": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"modifiedBy": {
"type": "string"
},
"modifiedDateTime": {
"type": "string"
},
"state": {
"type": "string"
}
},
"required": [
"id",
"displayName",
"description",
"createdBy",
"createdDateTime",
"modifiedBy",
"modifiedDateTime",
"state",
"identitySources"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
},
"runAfter": {
"Get_connectedOrganizations": [
"Succeeded"
]
},
"type": "ParseJson"
}
},
"runAfter": {
"Initialize_GraphQuery": [
"Succeeded"
]
},
"type": "Scope"
},
"Initialize_AllowedExternalUserDomains_variable": {
"inputs": {
"variables": [
{
"name": "AllowedExternalUserDomains",
"type": "array",
"value": []
}
]
},
"runAfter": {},
"type": "InitializeVariable"
},
"Initialize_GraphAuditLogActivityDateTimeInterval": {
"description": "The number of specified time units to subtract from current time when querying audit log for recent Guest Invite events",
"inputs": {
"variables": [
{
"name": "GraphAuditLogActivityDateTimeInterval",
"type": "integer",
"value": 7
}
]
},
"runAfter": {
"Initialize_extension_attribute_name_-_ExtensionApprovedForCollabName": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Initialize_GraphAuditLogActivityDateTimeUnit": {
"description": "The unit of time to use with time interval: \"Second\", \"Minute\", \"Hour\", \"Day\", \"Week\", \"Month\", \"Year\"",
"inputs": {
"variables": [
{
"name": "GraphAuditLogActivityDateTimeUnit",
"type": "string",
"value": "Day"
}
]
},
"runAfter": {
"Initialize_GraphAuditLogActivityDateTimeInterval": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Initialize_GraphQuery": {
"inputs": {
"variables": [
{
"name": "GraphQuery",
"type": "string",
"value": "https://graph.microsoft.com/beta/users?$top=999&$filter=UserType eq 'Guest' and createdDateTime ge @{getPastTime(variables('GraphAuditLogActivityDateTimeInterval'),variables('GraphAuditLogActivityDateTimeUnit'))}"
}
]
},
"runAfter": {
"Initialize_UserTenantId": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Initialize_UserDomain": {
"inputs": {
"variables": [
{
"name": "UserDomain",
"type": "string"
}
]
},
"runAfter": {
"Initialize_GraphAuditLogActivityDateTimeUnit": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Initialize_UserTenantId": {
"inputs": {
"variables": [
{
"name": "UserTenantId",
"type": "string"
}
]
},
"runAfter": {
"Initialize_UserDomain": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Initialize_extension_attribute_name_-_ExtensionApprovedForCollabName": {
"inputs": {
"variables": [
{
"name": "extensionApprovedForCollabName",
"type": "string",
"value": "extension_66a8b64982e34ab0b01dcde950ecf234_approvedForCollab"
}
]
},
"runAfter": {
"Initialize_AllowedExternalUserDomains_variable": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Process_All_Guest_Users": {
"actions": {
"Condition": {
"actions": {
"Process_Batch_of_Guests": {
"actions": {
"Process_Invited_Guest_Users": {
"actions": {
"Is_this_a_new_guest_user": {
"actions": {
"Get_User_Tenant_Info": {
"actions": {
"Clear_UserDomain": {
"inputs": {
"name": "UserDomain",
"value": "\"Clear\""
},
"runAfter": {
"Clear_UserTenantId": [
"Succeeded"
]
},
"type": "SetVariable"
},
"Clear_UserTenantId": {
"inputs": {
"name": "UserTenantId",
"value": "\"Clear\""
},
"runAfter": {},
"type": "SetVariable"
},
"Get_Domain_Info": {
"inputs": {
"method": "GET",
"uri": "@{concat('https://login.microsoftonline.com/',last(split(item()?['mail'], '@')),'/.well-known/openid-configuration')}"
},
"runAfter": {
"Set_UserDomain": [
"Succeeded"
]
},
"type": "Http"
},
"Parse_JSON": {
"inputs": {
"content": "@body('Get_Domain_Info')",
"schema": {
"properties": {
"authorization_endpoint": {
"type": "string"
},
"check_session_iframe": {
"type": "string"
},
"claims_supported": {
"items": {
"type": "string"
},
"type": "array"
},
"cloud_graph_host_name": {
"type": "string"
},
"cloud_instance_name": {
"type": "string"
},
"device_authorization_endpoint": {
"type": "string"
},
"end_session_endpoint": {
"type": "string"
},
"frontchannel_logout_supported": {
"type": "boolean"
},
"http_logout_supported": {
"type": "boolean"
},
"id_token_signing_alg_values_supported": {
"items": {
"type": "string"
},
"type": "array"
},
"issuer": {
"type": "string"
},
"jwks_uri": {
"type": "string"
},
"microsoft_multi_refresh_token": {
"type": "boolean"
},
"msgraph_host": {
"type": "string"
},
"rbac_url": {
"type": "string"
},
"response_modes_supported": {
"items": {
"type": "string"
},
"type": "array"
},
"response_types_supported": {
"items": {
"type": "string"
},
"type": "array"
},
"scopes_supported": {
"items": {
"type": "string"
},
"type": "array"
},
"subject_types_supported": {
"items": {
"type": "string"
},
"type": "array"
},
"tenant_region_scope": {
"type": "string"
},
"token_endpoint": {
"type": "string"
},
"token_endpoint_auth_methods_supported": {
"items": {
"type": "string"
},
"type": "array"
},
"userinfo_endpoint": {
"type": "string"
}
},
"type": "object"
}
},
"runAfter": {
"Get_Domain_Info": [
"Succeeded",
"Failed"
]
},
"type": "ParseJson"
},
"Set_UserDomain": {
"inputs": {
"name": "UserDomain",
"value": "@{last(split(item()?['mail'], '@'))}"
},
"runAfter": {
"Clear_UserDomain": [
"Succeeded"
]
},
"type": "SetVariable"
},
"Set_variable": {
"inputs": {
"name": "UserTenantId",
"value": "@{split(body('Parse_JSON')?['issuer'], '/')[3]}"
},
"runAfter": {
"Parse_JSON": [
"Succeeded",
"Failed"
]
},
"type": "SetVariable"
}
},
"runAfter": {},
"type": "Scope"
},
"Process_User": {
"actions": {
"Is_user's_domain_in_allowed_list": {
"actions": {
"Set_ApprovedForCollab_=_Yes_(if_it_hasn't_already_been_set)": {
"actions": {
"Set_Guest_User_ApprovedForCollab_=_Yes": {
"inputs": {
"authentication": {
"audience": "https://graph.microsoft.com",
"type": "ManagedServiceIdentity"
},
"body": {
"@{variables('extensionApprovedForCollabName')}": "Yes"
},
"method": "PATCH",
"uri": "https://graph.microsoft.com/beta/users/@{items('Process_Invited_Guest_Users')?['id']}"
},
"runAfter": {},
"type": "Http"
}
},
"expression": {
"and": [
{
"not": {
"equals": [
"@item()?[variables('extensionApprovedForCollabName')]",
"Yes"
]
}
}
]
},
"runAfter": {},
"type": "If"
}
},
"else": {
"actions": {
"Set_ApprovedForCollab_=_No_(if_it_hasn't_already_been_set)": {
"actions": {
"Set_Guest_User_ApprovedForCollab_=_No": {
"inputs": {
"authentication": {
"audience": "https://graph.microsoft.com",
"type": "ManagedServiceIdentity"
},
"body": {
"@{variables('extensionApprovedForCollabName')}": "No"
},
"method": "PATCH",
"uri": "https://graph.microsoft.com/beta/users/@{items('Process_Invited_Guest_Users')?['id']}"
},
"runAfter": {},
"type": "Http"
}
},
"expression": {
"and": [
{
"not": {
"equals": [
"@item()?[variables('extensionApprovedForCollabName')]",
"No"
]
}
}
]
},
"runAfter": {},
"type": "If"
}
}
},
"expression": {
"or": [
{
"or": [
{
"contains": [
"@variables('AllowedExternalUserDomains')",
"@last(split(item()?['mail'], '@'))"
]
},
{
"and": [
{
"greaterOrEquals": [
"@length(split(concat(body('Parse_JSON')?['issuer'],''), '/'))",
3
]
},
{
"contains": [
"@variables('AllowedExternalUserDomains')",
"@split(body('Parse_JSON')?['issuer'], '/')[3]"
]
}
]
}
]
}
]
},
"runAfter": {},
"type": "If"
}
},
"runAfter": {
"Get_User_Tenant_Info": [
"Succeeded",
"Failed"
]
},
"type": "Scope"
}
},
"expression": {
"and": [
{
"not": {
"contains": [
"",
"@variables('extensionApprovedForCollabName')"
]
}
}
]
},
"runAfter": {},
"type": "If"
}
},
"foreach": "@body('Parse_Guests_JSON')?['value']",
"runAfter": {},
"type": "Foreach"
}
},
"runAfter": {},
"type": "Scope"
}
},
"expression": {
"and": [
{
"greater": [
"@length(body('Parse_Guests_JSON')?['value'])",
0
]
}
]
},
"runAfter": {
"Get_Graph_NextLink": [
"Succeeded"
]
},
"type": "If"
},
"Get_Graph_NextLink": {
"inputs": {
"name": "GraphQuery",
"value": "@body('Parse_Guests_JSON')?['@odata.nextLink']"
},
"runAfter": {
"Parse_Guests_JSON": [
"Succeeded"
]
},
"type": "SetVariable"
},
"Graph_-_Get_Guest_Invites": {
"inputs": {
"authentication": {
"audience": "https://graph.microsoft.com",
"type": "ManagedServiceIdentity"
},
"method": "GET",
"uri": "@variables('GraphQuery')"
},
"runAfter": {},
"type": "Http"
},
"Parse_Guests_JSON": {
"inputs": {
"content": "@body('Graph_-_Get_Guest_Invites')",
"schema": {
"properties": {
"@@odata.context": {
"type": "string"
},
"@@odata.nextLink": {
"type": "string"
},
"value": {
"items": {
"properties": {
"accountEnabled": {
"type": "boolean"
},
"ageGroup": {},
"assignedLicenses": {
"type": "array"
},
"assignedPlans": {
"type": "array"
},
"businessPhones": {
"type": "array"
},
"city": {},
"companyName": {},
"consentProvidedForMinor": {},
"country": {},
"createdDateTime": {
"type": "string"
},
"creationType": {
"type": "string"
},
"deletedDateTime": {},
"department": {},
"deviceKeys": {
"type": "array"
},
"displayName": {
"type": "string"
},
"employeeHireDate": {},
"employeeId": {},
"employeeOrgData": {},
"employeeType": {},
"externalUserState": {
"type": "string"
},
"externalUserStateChangeDateTime": {
"type": "string"
},
"faxNumber": {},
"givenName": {},
"id": {
"type": "string"
},
"identities": {
"items": {
"properties": {
"issuer": {
"type": "string"
},
"issuerAssignedId": {
"type": "string"
},
"signInType": {
"type": "string"
}
},
"required": [
"signInType",
"issuer",
"issuerAssignedId"
],
"type": "object"
},
"type": "array"
},
"imAddresses": {
"type": "array"
},
"infoCatalogs": {
"type": "array"
},
"isManagementRestricted": {},
"isResourceAccount": {},
"jobTitle": {},
"legalAgeGroupClassification": {},
"mail": {
"type": "string"
},
"mailNickname": {
"type": "string"
},
"mobilePhone": {},
"officeLocation": {},
"onPremisesDistinguishedName": {},
"onPremisesDomainName": {},
"onPremisesExtensionAttributes": {
"properties": {
"extensionAttribute1": {},
"extensionAttribute10": {},
"extensionAttribute11": {},
"extensionAttribute12": {},
"extensionAttribute13": {},
"extensionAttribute14": {},
"extensionAttribute15": {},
"extensionAttribute2": {},
"extensionAttribute3": {},
"extensionAttribute4": {},
"extensionAttribute5": {},
"extensionAttribute6": {},
"extensionAttribute7": {},
"extensionAttribute8": {},
"extensionAttribute9": {}
},
"type": "object"
},
"onPremisesImmutableId": {},
"onPremisesLastSyncDateTime": {},
"onPremisesProvisioningErrors": {
"type": "array"
},
"onPremisesSamAccountName": {},
"onPremisesSecurityIdentifier": {},
"onPremisesSyncEnabled": {},
"onPremisesUserPrincipalName": {},
"otherMails": {
"items": {
"type": "string"
},
"type": "array"
},
"passwordPolicies": {},
"passwordProfile": {},
"postalCode": {},
"preferredDataLocation": {},
"preferredLanguage": {},
"provisionedPlans": {
"type": "array"
},
"proxyAddresses": {
"items": {
"type": "string"
},
"type": "array"
},
"refreshTokensValidFromDateTime": {
"type": "string"
},
"showInAddressList": {
"type": "boolean"
},
"signInSessionsValidFromDateTime": {
"type": "string"
},
"state": {},
"streetAddress": {},
"surname": {},
"usageLocation": {},
"userPrincipalName": {
"type": "string"
},
"userType": {
"type": "string"
}
},
"required": [
"id",
"displayName",
"mail",
"userPrincipalName",
"userType"
],
"type": "object"
},
"type": "array"
}
},
"type": "object"
}
},
"runAfter": {
"Graph_-_Get_Guest_Invites": [
"Succeeded"
]
},
"type": "ParseJson"
}
},
"expression": "@equals(length(variables('GraphQuery')), 0)",
"limit": {
"count": 60,
"timeout": "PT1H"
},
"runAfter": {
"Get_Connected_Orgs": [
"Succeeded"
]
},
"type": "Until"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {},
"triggers": {
"Recurrence": {
"recurrence": {
"frequency": "Minute",
"interval": 10
},
"type": "Recurrence"
}
}
},
"parameters": {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment