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

Azure AD Application Proxy – SSO and Authorization notes from the field

This blog attempts to capture some of the Single Sign-On and Authorization scenarios I’ve dealt with during my extensive tenure with Azure AD Application proxy deployments.

To keep this blog short there is no description of what Azure AD Application Proxy is generally. More information can be found here

  • AAD SSO Column in the table indicates whether Single Sign-On setting on Azure AD Enterprise Application is enabled or disabled
  • Native Clients in the following table assumes scenario where browser-like features (web view etc) are only used to perform subset of functions, such as authenticating user to Azure AD. All other calls are authorized to Azure AD Application via the usage of Access Tokens passed in Authorization Header
    • For example: The native app may not use cookies to hold session persistence, but rather refresh token stored in the device
    • Some native clients rely completely on running browser inside the app, or open external browser (payment views etc). In these scenarios the application may support similar option as described in the second column (2 Browser)


Back-end Protocol
AAD SSO1 Native Client 2 Browser
SAML – WS/FED (SSO)*Enabled:SAML / or Disabled*No – At least when the most common binding: (Redirect -> POST) is used
Yes 1.Further reading 2.Source
Header Based Authentication against back-end APIDisabledYes With Azure API management using JWT_Bearer Grant Yes With Azure API management using JWT_Bearer Grant (Note this solution is not for rendering browser views. To render browser view from API response additional client side rendering is required
OAuth2 / OIDC Disabled No, unless the application is using web-view that maintains the session to Azure AD (source) Yes – While existing Azure AD session is maintained within browser. Fetch/XHR, or redirect based client can make requests to the back-end app, which can then validate tokens in Authorization Bearer scenario (source)
Header Based Authentication using PingAccessEnabled:Header-BasedNot tested – Probably? (Refer to the documentation) Not tested – Probably? (Refer to the documentation)
Windows integrated Authentication / SPNEGO * * Enabled:Windows Integrated Authentication
Yes – Kerberos Constrained Delegation by the App Proxy Agent is transparent to the client
Yes – Kerberos Constrained Delegation by the App Proxy Agent is transparent to the client
Basic / NTLM Authentication  * * * Disabled No – Authorization header is reserved for Bearer Tokens, which App Proxy Consumes Yes – While existing Azure AD session is maintained within browser, Basic Authentication can be used. NTLM can be used as well, applies also to WIA scenario when WIA fallbacks to NTLM
SOAP API (U/P)Disabled
Yes – When credentials are passed as part of body, and authorization header contains the Bearer Token
Yes – When credentials are passed as part of body, and authorization header contains the Bearer Token
Client CertificateNANo No

Clarifications

  • * Existing IDP Session
    • You could also have specified another Azure AD application separate from the App Proxy Enterprise Application. The SSO works regardless of the SSO setting, when the application uses sames IDP which the user has existing session on.
  • * * About SPNEGO [Source]
    • ”If you configure a connector machine for SPNEGO, make sure that all other connectors in that Connector group are also configured with SPNEGO. Applications expecting standard KCD should be routed through other connectors that are not configured for SPNEGO ”
  • * * * Compound Authorization (in some cases Double authentication)
    • First Authorization: Azure AD Consumes the Authorization header
      • While Sending additional Bearer token in other than Authorization header, or in body payload
      • Or While Sending additional Credentials in other than Authorization header, or in body payload
      • Sequentially (after) challenged by the back-end app running in browser session

Good to know

  • Native API clients using AAD Pre-authentication
    • With Native Clients Forwarding JWT tokens to back-end service doesn’t work unless the Native client is calling Azure AD App Proxy from webview like representation or pure browser
  • Only user identities can passed through application having Azure AD pre-authentication enabled
    • For example Application identity (Client Credentials Flow) doesn’t work with Azure AD App Proxy Applications, unless Compound Authorization is used
    • Apps can still work in chain (On-Behalf-of-Flow for example) as long as the identity being delegated is user identity
  • If you have Windows Authentication enabled in the Application, ensure the deployment scenario supports Kerberos Constrained Delegation, and the Azure AD App Proxy agent can act as delegating party to the SPN.
  • While Mutual TLS authentication isn’t supported features such as require device compliance from and other Client recognizing options are available when combined with Conditional Access.
  • There are also other scenarios which I may cover in another blog post
    • Using a mix of pre-authenticated and pass-trough Azure AD Applications
    • Linked applications
    • Password-based

Further reading

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-configure-single-sign-on-with-kcd

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-configure-for-claims-aware-applications

If you find any discrepancies (errors etc) with the information on the table just dm me at @santasalojoosua, or post question to this blog, and I shall correct it /explain it.

Experimental testing: Azure AD Application Proxy With Azure Application Gateway WAF

Disclaimer: This configuration example is only for experimental testing. I’d advise against using it in any kind of serious scenario as the configuration has no official support …and is based on-whim testing 🙂

I was recently browsing Feedback for Azure AD Application Proxy, and noticed that I am not the only one who would like to see WAF functionality enabled for AAD App Proxy.

The comment for ”Under Review” raised my curiosity ” We are reviewing options for creating smoother integration and providing documentation on how to layer the two. ”
https://feedback.azure.com/forums/169401-azure-active-directory/suggestions/31964980-allow-azure-ad-app-proxy-apps-to-use-the-azure-web

While its fairly easy to retrofit WAF API -scenario with Azure AD App Proxy and API management, it’s another thing to also make it render web pages in a browser without a custom front end. https://securecloud.blog/2019/06/01/concept-publish-on-prem-api-using-aad-app-proxy-and-api-management-with-azure-ad-jwt-bearer-grant/

Test configuration

Application Proxy Configuration

Application Gateway Configuration

  1. Create Listener binding the cert for App Proxy Apps FQDN

2. Add the IP of Azure AD App Proxy as back-end target

  • The logic: Point the DNS to Application Gateway instead to App Proxy Application, and point the application gateway to that CNAME, and override the naming bind in the listener of Application Gateway
Use the name AppProxy DNS should be pointed at

3. Override the host name to the same name that is in the DNS (this would create loop, unless we hadn’t different name in the back-end pool)

Now watch the back end for traffic originating through WAF + AppProxy

Back-end application receiving WAF forwarded traffic, with both App Proxy and Application Gateway headers
  • Obvious problem is that the attacker can bypass WAF by ”gatewaying” itself with custom DNS directly to the AppProxy.
    • Obviously there is no public reference anywhere, what is the IP for Azure AD App Proxy app, or whats the name of the app, as the communication goes through App GW, and DNS points to App GW. Depending on the back-end app, the attacker might figure out a simple way, to get the app ”echoing” back the route (For example headers…)
  • Sub optimal mitigations would be (if back-end app is configurable, and you want to check in back end that did the request come from WAF)
      • that the only calls that have last X-Forwarded-For IP as Application Gateway would be authorized.
      • Or to set an ”secret” header in Application Gateway URL rewrite rules, and check the presence of that header in the back-end app for authorization
  • If I had to do this in production today, I would place WAF in the internal network before the back-end app
AppGW WAF headers
The app is the consent extractor, which i just used as placeholder app (has no context meaning in this scenario)

I will stay tuned to see if this feature gets actually implemented!

Br Joosua!

Reddit Thread Answer: Azure AD – Autologon endpoint

This is an endpoint which is not used independently, but rather as a sub-feature of another feature in Azure AD. The feature is Azure AD Seamless SSO, which you can find out in general more by clicking the link in the picture

Seamless Single Sign On - Web app flow
https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sso-how-it-works#how-does-sign-in-on-a-native-client-with-seamless-sso-work

Autologon endpoint is used to facilitate particular flows of Azure AD Seamless SSO. One of the flows provides WS-Trust endpoint via metadata query. This endpoint is available for managed account types on all global Azure AD tenants. This is regardless if Seamless SSO is enabled or not.

AFAIK the flow isn’t meant to be used outside of the Seamless SSO scenarios. ROPC (Resource Owner Password Flow) would be the recommended flow if you absolutely need to transport username and password outside of the browsers input flow (passive ”profiles”)

Example: Get token from UserNameMixed endpoint, and exchange the SAML Token (DesktopSSO) for OAuth2 Access Token

[string]$username= Read-Host -Prompt "Enter UserName"
$securedValue = Read-Host -AsSecureString -Prompt "uurPass"
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securedValue)
$password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
$requestid = [System.Guid]::NewGuid().guid
$domain = ($username -split "@")[1]
Invoke-RestMethod -Method Get -UseBasicParsing ("https://login.microsoftonline.com/common/userrealm/$username" + "?api-version=1.0") -UserAgent $userAgent
$headers = @{
"client-request-id"=$requestid
"return-client-request-id"="true"
}
$uri2 = "https://autologon.microsoftazuread-sso.com/$domain/winauth/trust/2005/usernamemixed?client-request-id=$requestid"
[xml]$data = '<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
    <a:MessageID>urn:uuid:36a6762f-40a9-4279-b4e6-b01c944b5698</a:MessageID>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
    <a:To s:mustUnderstand="1">https://autologon.microsoftazuread-sso.com/dewi.onmicrosoft.com/winauth/trust/2005/usernamemixed?client-request-id=30cad7ca-797c-4dba-81f6-8b01f6371013</a:To>
    <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
      <u:Timestamp u:Id="_0">
        <u:Created>2019-01-02T14:30:02.068Z</u:Created>
        <u:Expires>2019-01-02T14:40:02.068Z</u:Expires>
      </u:Timestamp>
      <o:UsernameToken u:Id="uuid-ec4527b8-bbb0-4cbb-88cf-abe27fe60977">
        <o:Username>DefinedLater</o:Username>
        <o:Password>DefinedLater</o:Password>
      </o:UsernameToken>
    </o:Security>
  </s:Header>
  <s:Body>
    <trust:RequestSecurityToken xmlns:trust="http://schemas.xmlsoap.org/ws/2005/02/trust">
      <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
        <a:EndpointReference>
          <a:Address>urn:federation:MicrosoftOnline</a:Address>
        </a:EndpointReference>
      </wsp:AppliesTo>
      <trust:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</trust:KeyType>
      <trust:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</trust:RequestType>
    </trust:RequestSecurityToken>
  </s:Body>
</s:Envelope>
'
[string]$UsernameToken  = [System.Guid]::NewGuid().guid
[string]$messageId = "urn:uuid:" + ([System.Guid]::NewGuid().guid)
 $data.Envelope.Header.Security.UsernameToken.Id =$UsernameToken
 $data.Envelope.Header.Security.UsernameToken.Username = $username
 $data.Envelope.Header.Security.UsernameToken.Password = $password
 $data.Envelope.Header.MessageID = $messageId
 $data.Envelope.Header.To.'#text'= $uri2
$req = Invoke-RestMethod -UseBasicParsing -Uri $uri2 -Method Post -Headers $headers -Body  $data -ContentType "application/soap+xml; charset=utf-8" -UserAgent $userAgent 
$samltoken = $req.Envelope.Body.RequestSecurityTokenResponse.RequestedSecurityToken.Assertion.DesktopSsoToken
$token ='<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"><DesktopSsoToken>SAMLSSO</DesktopSsoToken></saml:Assertion>' -replace "SAMLSSO", $samltoken
$bytes = [System.Text.Encoding]::ASCII.GetBytes($token)
$base64 = [System.Convert]::ToBase64String($bytes);$base64
$uri3 = "https://login.microsoftonline.com/common/oauth2/token"
$body =@{
    client_id="cb1056e2-e479-49de-ae31-7812af012ed8"
    resource="https://graph.microsoft.com"
    grant_type="urn:ietf:params:oauth:grant-type:saml1_1-bearer"
    assertion=$base64
    }
    
$req = Invoke-RestMethod -UseBasicParsing -Uri $uri3 -Method Post -Headers $headers -ContentType "application/x-www-form-urlencoded" -Body $body
$headers = @{
    "Authorization" = ($req.token_type) +" "+ ($req.access_token)
    }
    
    $me = Invoke-RestMethod -Uri ($body.resource + "/v1.0/me") -Method Get -Headers $headers; $me