AAD Security made easy: Check your Azure AD Security with One-Liner (AZSK.AAD)

You can now scan your AAD tenant security posture with one-liner, and get really detailed and comprehensive report of the results


Install-Module AzSK.AAD -Scope CurrentUser -AllowClobber 
Import-Module AzSK.AAD 
<#Controls available

The following controls are available from the get go!

AAD_Tenant_RBAC_Grant_Limited_Access_To_GuestsGuests must not be granted full access to the directory
AAD_Tenant_RBAC_Dont_Permit_Guests_To_Invite_GuestsGuests must not be allowed to invite other guests
AAD_Tenant_MFA_Required_For_AdminsAdmins must use baseline MFA policy
AAD_Tenant_Apps_Dont_Allow_Users_To_Create_AppsDo not permit users to create apps in tenant
AAD_Tenant_RBAC_Dont_Allow_Users_To_Invite_GuestsDo not permit users to invite guests to the tenant
AAD_Tenant_Misc_Set_Security_Contact_InfoSecurity compliance notification phone and email must be set
AAD_Tenant_Device_Require_MFA_For_JoinEnable ’require MFA’ for joining devices to tenant
AAD_Tenant_Device_Set_Max_Per_User_LimitSet a max device limit for users in the tenant
AAD_Tenant_MFA_Review_Bypassed_UsersReview list of current ’MFA-bypassed’ users in the tenant
AAD_Tenant_MFA_Allow_Users_To_Notify_About_FraudAllow users to send notifications about possible fraud
AAD_Tenant_Apps_Regulate_Data_Access_ApprovalDo not allow users to approve tenant data access for external apps
AAD_Tenant_RBAC_Keep_Min_Global_AdminsInclude at least three members in global admin role
AAD_Tenant_RBAC_Dont_Have_Guests_As_Global_AdminsGuest users must not be made members of global admin role
AAD_Tenant_AuthN_Use_Custom_Banned_PasswordsEnsure that custom banned passwords list is configured for use
AAD_Tenant_AuthN_Enforce_Banned_Passwords_OnPremEnsure that banned password check is enabled on-prem and set to ’Enforce’ level
AAD_Tenant_Privacy_Configure_Valid_Privacy_ContactEnsure that tenant-wide privacy contact email is set to a valid (current) non-guest user
AAD_Tenant_Privacy_Configure_Valid_Privacy_StatementEnsure that a privacy statement is configured and points to a valid URL
AAD_Application_Remove_Test_Demo_AppsOld test/demo apps should be removed from the tenant
AAD_Application_ReturnURLs_Use_HTTPSAll return URLs configured for an application must be HTTPS endpoints
AAD_Application_Review_Orphaned_AppsDo not permit orphaned apps (i.e., apps with no owners) in the tenant
AAD_Application_Require_FTE_OwnerAt least one of the owners of an app must be an FTE
AAD_Application_HomePage_Use_HTTPSThe home page URL for an application must be an HTTPS endpoint
AAD_Application_LogoutURLs_Use_HTTPSThe logout URL configured for an application must be an HTTPS endpoint
AAD_Application_Must_Have_Privacy_DisclosureAll enterprise apps must use a privacy disclosure statement
AAD_Application_Must_Restrict_To_TenantEnterprise (line of business) apps should be tenant scope only
AAD_Application_Minimize_Resource_Access_RequestedApps should request the least permissions needed to various resources
AAD_ServicePrincipal_Use_Cert_CredentialsSPNs must not use password creds – use cert creds instead
AAD_ServicePrincipal_Review_Legacy_SPNSPNs of type legacy should be carefully reviewed
AAD_ServicePrincipal_Check_Key_ExpirySPN key credentials should be renewed before expiry
AAD_Device_Review_Stale_DevicesReview and remove stale devices from the directory
AAD_User_DirSync_Setting_Should_Match_TenantA user’s dirsync-enabled setting must match the tenant level setting
AAD_User_Do_Not_Disable_Password_ExpirationDo not disable password expiration policy for users
AAD_User_Do_Not_Disable_Strong_PasswordDo not disable strong password policy for users
AAD_Group_Use_Security_EnabledAll AAD groups must be security enabled (TBD)
AAD_Group_Require_FTE_OwnerGroup must have at least one non-guest (native) owner

Add sAMAccountName to Azure AD Access Token (JWT) with Claims Mapping Policy (and avoiding AADSTS50146)

With the possibilities available (and quite many of blogs) regarding the subject), I cant blame anyone for wondering whats the right way to do this. At least I can present one way that worked for me

Here are the total ways to do it (1. obviously not the JWT token)

  1. With SAML federations you have full claims selection in GUI
  2. Populate optional claims to the API in app registration manifest, given you’ve updated the schema for the particular app
  3. Create custom Claims Policy, to choose emitted claims (The option we’re exploring here)
  4. Query the directory extension claims from Microsoft Graph API appended in to the directory schema extension app* that Graph API can call

Please note, for sAMAccountName we’re not using the approach where we add directory extensions to Graph API queryable application = NO DIRECTORY EXTENSION SYNC IN AAD CONNECT NEEDED

Checklist for using Claims Mapping Policy

Pre: Have Client application, and web API ready before proceeding

#Example App to Add the Claims 
$Definition = [ordered]@{
    "ClaimsMappingPolicy" = [ordered]@{
        "Version" = 1
        "IncludeBasicClaimSet" = $true
        "ClaimsSchema" = @(
                "Source" = "user"
                "ID" = "onpremisessamaccountname"
                "JwtClaimType" = "onpremisessamaccountname"
$pol =  New-AzureADPolicy -Definition ($definition | ConvertTo-Json -Depth 3) -DisplayName ("Policy_" + ([System.Guid]::NewGuid().guid) + "_" + $template.Values.claimsschema.JwtClaimType) -Type "ClaimsMappingPolicy" 
$entApp =  New-AzureADApplication -DisplayName  ("DemoApp_" + $template.Values.claimsschema.JwtClaimType)
$spnob =  New-AzureADServicePrincipal -DisplayName $entApp.DisplayName -AppId $entApp.AppId 
Add-AzureADServicePrincipalPolicy -Id $spnob.ObjectId -RefObjectId $pol.Id 
#From the GUI change the Identifier and acceptMappedClaims value (From the legacy experience)

  • Generally: The app that will emit the claims is not the one you use as the clientID (Client subscribing to the Audience)
    • Essentially you should create un-trusted client with clientID, and then add under Api permissions the audience/resource you’re using
  • Ensure that SPN has IdentifierURI that matches registered custom domain in the tenant
    • The reasoning is vaguely explained here & here
      • Whatever research work the feedback senders did, it sure looked in depth 🙂
  • Update the app manifest to Accept Mapped Claims
    • Do this in the legacy experience, the new experience at least in my tenant didn’t support updating this particular value
”Insufficient privileges to complete the operation”

if mapped claims are not accepted in manifest, and pre-requisites are not satisfied you might get this error

”AADSTS50146: This application is required to be configured with an application-specific signing key. It is either not configured with one, or the key has expired or is not yet valid. Please contact the application’s administrator.”

  • Below is example for the Manifest changes (AcceptMappedClaims, and verified domain matching URI)
     "id": "901e4433-88a9-4f76-84ca-ddb4ceac8703",
    "acceptMappedClaims": true,
    "accessTokenAcceptedVersion": null,
    "addIns": [],
    "allowPublicClient": null,
    "appId": "9bcda514-7e6a-4702-9a0a-735dfdf248fd",
    "appRoles": [],
    "oauth2AllowUrlPathMatching": false,
    "createdDateTime": "2019-06-05T17:37:58Z",
    "groupMembershipClaims": null,
    "identifierUris": [


If you’re planning to use non-verified domain based identifier

”AADSTS501461: AcceptMappedClaims is only supported for a token audience matching the application GUID or an audience within the tenant’s verified domains.







Decode JWT access and id tokens via PowerShell

Concept: Publish on-prem API using AAD App Proxy and API Management with Azure AD JWT Bearer Grant

Disclaimer: Azure AD App Proxy is perfectly capable of covering most of the internal API publishing scenarios, If you can handle API request and response handling with just client and on-premises server. Alternatively you might have another component on-prem which can act as middle-tier component to do further validation and shaping of requests. In a nutshell, if you’re API-scenario doesn’t benefit from from middle-tier service, then I suggest you continue with ”keeping-it-simple” /And as always, all Disclaimer: The information in this weblog is provided “AS IS” with no warranties and confers no rights.

Better together?

API Management and AAD App Proxy can complement each other when you need to have request shaping / central API Gateway for processing before calling back-end API’s – In this blog I explore a PoC example, and some reasoning for such scenarios

When to use? / benefits

  • You’re internal API isn’t visible to Azure API management via on-premises network connectivity, and you’re not planning to use site-site networking in the future, or for a particular API
  • You want to enrich payloads and headers of requests for particular back-end services. For example services which cant consume claims in JWT Tokens. You also want to ensure, that selected parts of these payloads cannot be forged by the client
  • Have single end-point to distribute and shape/manipulate traffic to various API’s (General argument)
Extra Claims from APIM

How it works (short)

  • APIM calls the App Proxy SPN instead of mobile clients
  • The ServicePrincipals for APIM and AppProxy App are blocked for providing access tokens via Authorization Code Grant (removal of redirect uri). Leaving only option that APIM will use the JWT-Bearer grant. This ensures, that only the APIM can fetch the ”final” access token, for the App Proxy App
  • Flow: Native client (Auth Code Flow) -> APIM (JWT-Bearer Grant) -> Azure AD App Proxy SPN Authorization (Permissions to make this work are explained on later part of this blog)

Ensuring integrity with retrofit of AAD App Proxy & APIM

  • In order to retrofit the Azure AD Application Proxy with APIM it’s essential that the App Proxy Application and APIM SPN can act only as as Web API’s (not public clients) this keeps the flow intact
    • Stripping token issuance rights of the SPN for Authorization Code flow, ensures: That only the App registration for APIM can delegate user access to the App Proxy SPN (Audience) using that particular flow
    • This assumption only works when you don’t retroactively enable Implicit Flow on the SPN itself. (You can have Implicit grant on other clients, but not on the this particular SPN, which is the owner of the AppProxy Audience ( identifierUri)

How To?

  • Remove redirect URI’s from both middle-stream API and App Proxy Application
  • Then perform the fencing (below) by delegating rights in correct order to support the flow
  • Now the public client, can only get tokens for APIM, but can never call App Proxy directly, as the client doesn’t have direct permissions on the App Proxy SPN (Only the APIM has)

Azure AD Fencing: Utilizing extended grant types to access downstream API’s

Click the picture for bigger version
OAuth2.0 On-Behalf-Of flow

Using Azure AD Specific GrantType (urn:ietf:params:oauth:grant-type:jwt-bearer) is the magic component we use here.

  • JWT bearer flow allows us to create ”DMZ-like” fencing between direct calls, and downstream calls destined AppProxy SPN with Middle-tier API

Using the On-Behalf-Of flow (JWT bearer), we can ensure that APIM is the only allowed caller for the App Proxy Audience

Click the picture for bigger version

API Management configuration

  • The following policy is ”tip of the ice berg” in terms of how you can shape, and handle requests bound to multiple directions
  • There is possibility of doing more graceful handling with more the multiple policy clauses APIM provides
  • I can hardly claim any credit (apart architectural and flow design) for the APIM policies below, as web is full great APIM examples for all of the policies I have used below
        <!-- validate the initial call destined later towards middle-tier API-->
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
            <openid-config url="https://login.microsoftonline.com/dewired.onmicrosoft.com/.well-known/openid-configuration" />
                <claim name="appid" match="any">
        <!-- Forward THE UPN header to back-end -->
        <set-variable name="UPN" value="@(context.Request.Headers["Authorization"].First().Split(' ')[1].AsJwt()?.Claims["upn"].FirstOrDefault())" />
        <set-variable name="Bearer" value="@(context.Request.Headers["Authorization"].First().Split(' ')[1])" />
        <set-header name="back-endUPN" exists-action="override">
        <!-- Send new request with the Token -->
        <send-request mode="new" response-variable-name="OBOtoken" timeout="20" ignore-error="false">
            <set-header name="Content-Type" exists-action="override">
            <set-header name="User-Agent" exists-action="override">
                <value>Mozilla/5.0 (Windows NT; Windows NT 10.0; fi-FI) WindowsPowerShell/5.1.17763.503</value>
            var tokens = context.Variables.GetValueOrDefault<string>("Bearer");
              return "assertion=" + tokens + @"&client_id={{clientid2}}&resource={{resource}}&client_secret={{ClientSecret}}&grant_type={{grantType}}&requested_token_use={{requested_token_use}}";
        <!-- Forward the OBOtoken to AppProxy  -->
            <when condition="@(((IResponse)context.Variables["OBOtoken"]).StatusCode == 200)">
                <set-variable name="OBOBearer" value="@(((IResponse)context.Variables["OBOtoken"]).Body.As<JObject>(preserveContent: true).GetValue("access_token").ToString())" />
                <set-variable name="Debug" value="@(((IResponse)context.Variables["OBOtoken"]).Body.As<JObject>(preserveContent: true).ToString())" />
                <set-header name="Authorization" exists-action="override">
                    var ForwardToken = context.Variables.GetValueOrDefault<string>("OBOBearer");
                    return "Bearer "+ ForwardToken;
                <set-header name="Content-Type" exists-action="override">
        <base />
        <base />
        <base />
        <base />

Testing the solution in action

Before testing ensure that all places where you’ve defined audiences are explicitly matching for the audience (App registrations, Named Values, APIM policies, Clients requesting the access)

There are multiple ways to test the solution, but testing through APIM’s test console, and peeking the in-secure back-end resource via HTTP trace yields the most verbose results

  • Get Access Token for the API-A (APIM) with ”bulk client”
  • Paste the access token to APIM test console and perform test call to view traces
  • Check the back-end for results
Click for bigger picture

And that’s it!

Further stuff

  • Use keyVault instead of named values (secret) for storing secrets
named values
  • Place WAF to the front of APIM
  • fine tune to the policies in APIM ( this was just the PoC)
    • For example, the back-end could use cached token for the downstream call, as the user, and user-identity is validated in the first step (its validated in the second step also)
  • Get some proficiency in C# syntax … As PS and JavaScript fellow, I found myself seriously struggling to properly escape, cast and enumerate variables/content

P.S. My similar articles