Securing Client Credentials Flow with Certificate

Sometimes there is application which requires background jobs to run without user interaction (though in most cases you can manage to avoid using App Permissions) In these particular exceptions using app permissions and client credentials might be justified, especially if you don’t want to maintain (and secure) long term storage of user refresh token for non user context operation.

Click the picture for larger version

Prevalent use of Shared Secrets in Client Credentials

I often see Client Credentials used with shared secret, and do understand that for some SaaS integrations with self service of on-boarding the shared secret is the easy way to on-board them. (Although uploading the private key for the particular app is not rocket science either, could be easily used in most of the cases)

Why Certificate is better than Shared Secret

Certs and Secrets show in Azure AD Application
  • Certificate Credentials never transmit the plain-text secret when requesting Access Tokens from Azure AD. Instead they transit JWT token which is signed with private key which the app holds. Verification is asymmetric, so Azure AD holds only the key which can assert that the JWT token came from the party in posession of the private key
  • Shared Secret in essence is weaker verification method (String vs Certificate)
  • You have more established ways to protect the certificate, than a single string

Alternatives to a certificate

  • You can use API management in conjunction with JWT-Bearer flow to gain better control of Shared Secret of 3rd parties.
    • This could be done by restricting caller IP’s and using several policies concerning the use Partners given access to Client Crendentials. In this scenario API management forwards the token once policies are checked
  • You could put short lived (few days) Client Secret in a Key Vault, and authorize the Key Vault answer only to a certain trusted IP’s… Still…once the plain text is exposed in the code run time the Client Secret is out of Key Vaults Domain until the client secret expires
    • Generally Client Secrets aren’t meant to be used as short lived
https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/overview
  • … And if the API doesn’t need App Permissions, you can use Conditional Access…
  • You shouldn’t use real user service account as service account just get conditional access. Using a user principal instead of actual service principal opens another set of problems, which this blog is too short to delve on

Enable NodeJS API for Client Credentials with Certificate

Before proceeding using App Permissions, or Shared Secret in any other flow just check that your scenario is not one of the below Examples

  1. App Permissions used in flows that could’ve used delegated permissions using users token context
  2. Mobile Client Using Client Secret as part of Authorization Code Flow (Mobile client is not confidential client, lets just leave it there)
  3. (Not directly related, but a concern) App Permissions are required by multi-tenant app
    • In most scenarios you can create additional single tenant app, besides the registered multi-tenant to retain control of the shared secret (revokation etc).
      • Giving external multi-tenant app permissions is something you should think hard before proceeding in the first place

Create Certificate Credentials for existing App Registration

Pre-hardening

  • Ensure Application doesn’t have any redirect URI’s. This effectively ensures no user sign in process can get tokens returned for the Application
  • Remove the default delegated user permissions from the app
  • Ensure Implicit Grant isn’t enabled in the application (This wouldn’t work any way with the user permissions removed to sign-in and read user profile , but we do some additional cleaning here)
  • Remove Any password credentials app might have (obviously, if they are used production, dont remove them until the flow is updated to use certificate in code for these apps)

Pre-reqs

  • OpenSSL binaries
  • Azure AD PowerShell Module
$Subject = "CN=" + "DemoApplication2"
$Expiration = (get-date).AddYears(2)
$pass = Read-Host -Prompt "PFX exporting Password"
$cert = New-SelfSignedCertificate -CertStoreLocation "Cert:\CurrentUser\My" -Subject $Subject -KeySpec KeyExchange -NotAfter $Expiration
$AADKeyValue = [System.Convert]::ToBase64String($cert.GetRawCertData())
$cert| Export-PfxCertificate -FilePath (($Subject -split "=")[1] + ".pfx") -Password ($pass | ConvertTo-SecureString -AsPlainText -Force) 
#Navigate to directory where OpenSSL is installed
.\openssl.exe pkcs12 -in (($Subject -split "=")[1] + ".pfx") -passin "pass:$pass"  -out (($Subject -split "=")[1] + ".pem") -nodes
$data = get-content (($Subject -split "=")[1] + ".pem")
$data[$data.IndexOf("-----BEGIN PRIVATE KEY-----")..$data.IndexOf("-----END PRIVATE KEY-----")] | Out-File (($Subject -split "=")[1] + ".pem") -encoding "DEFAULT"
connect-azuread
$application = New-AzureADApplication -DisplayName ($Subject -split "=")[1]
New-AzureADApplicationKeyCredential -ObjectId $application.ObjectId -CustomKeyIdentifier ($Subject -split "=")[1] -Type AsymmetricX509Cert -Usage Verify -Value $AADKeyValue -EndDate ($Expiration | get-date -format "dd.M.yyyy" )

NodeJS Code Example using ADAL and Certificate Credentials

  • In the code fill the thumprint, clientID (appid), and your tenant name
//Gets Access Token by sending private key signed JWT Token to Azure AD Token Endpoint 
//https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials
//https://www.npmjs.com/package/adal-node
const {AuthenticationContext,Logging} = require("adal-node")
//set logging level to verbose, and print to console 
Logging.setLoggingOptions({
    log: (level, message, error) => {
      console.log(message)
    },
    level: Logging.LOGGING_LEVEL.VERBOSE,
    loggingWithPII: true
  });
//Token Request Options
  var options = {
    URLwithTenant:"https://login.windows.net/dewired.onmicrosoft.com",
    resource:"https://graph.microsoft.com", 
    applicationId : "c81ea829-d488-46c2-8838-68dde9052478",
    certThumbPrint:"72B807BE590A73E0B88947C902C5C58E22344C5F"
  }
//Construct the Authentication Context
var context = new AuthenticationContext(options.URLwithTenant);
//Read Certificate from buffer to UTF8 string 
const fs = require("fs")
var keyStringFromBuffer = fs.readFileSync("DemoApplication2.pem").toString("UTF8")
console.log(keyStringFromBuffer)
//acquireToken
context.acquireTokenWithClientCertificate(options.resource,options.applicationId,keyStringFromBuffer,options.certThumbPrint,(error,tokenresponse) => {
console.log(error,tokenresponse)
})

In the end you should see verbose message with token returned in the callback for tokenResponse.

If you see error about the self signed cert, ensure that all localization settings match UTF8, and that there are no empty space characters in the PEM file. If you still see the error, copy the Private Key manually from the openSSL created file

Br, Joosua

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

https://github.com/azsk/DevOpsKit-docs/blob/master/ReleaseNotes/LatestReleaseNotes.md

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

The following controls are available from the get go!

ControlIDDescription
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 
AzureADPreview\Connect-AzureAD
$Definition = [ordered]@{
    "ClaimsMappingPolicy" = [ordered]@{
        "Version" = 1
        "IncludeBasicClaimSet" = $true
        "ClaimsSchema" = @(
            [ordered]@{
                "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": [
        "https://samajwt.dewi.red"
    ],

Testing

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.

References

https://github.com/MicrosoftDocs/azure-docs/issues/5394

https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/develop/active-directory-claims-mapping.md

https://github.com/Azure-Samples/active-directory-dotnet-daemon-certificate-credential#create-a-self-signed-certificate

https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/develop/v2-protocols-oidc.md#fetch-the-openid-connect-metadata-document

https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/active-directory/develop/active-directory-claims-mapping.md#example-create-and-assign-a-policy-to-include-the-employeeid-and-tenantcountry-as-claims-in-tokens-issued-to-a-service-principal

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 to act as clients providing access tokens via Authorization Code Grant (removal of redirect uri). Leaving only option that APIM will use the JWT-Bearer grant when acting as client towards app proxy. 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
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#protocol-diagram

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
 
<policies>
    <inbound>
        <!-- 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" />
            <audiences>
                <audience>https://webapi-a.dewi.red</audience>
            </audiences>
            <required-claims>
                <claim name="appid" match="any">
                    <value>299568d2-3036-41d9-a961-89266e67ea82</value>
                </claim>
            </required-claims>
        </validate-jwt>
        <!-- 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">
            <value>@(context.Variables.GetValueOrDefault<string>("UPN"))</value>
        </set-header>
        <!-- Send new request with the Token -->
        <send-request mode="new" response-variable-name="OBOtoken" timeout="20" ignore-error="false">
            <set-url>{{tokenURL2}}</set-url>
            <set-method>POST</set-method>
            <set-header name="Content-Type" exists-action="override">
                <value>application/x-www-form-urlencoded</value>
            </set-header>
            <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>
            </set-header>
            <set-body>@{
            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}}";
             
               }</set-body>
        </send-request>
        <!-- Forward the OBOtoken to AppProxy  -->
        <choose>
            <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">
                    <value>@{
                    var ForwardToken = context.Variables.GetValueOrDefault<string>("OBOBearer");
                    return "Bearer "+ ForwardToken;
                    }</value>
                </set-header>
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
            </when>
        </choose>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies> 

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)
https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-cache
  • 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