Azure Functions Calling Azure AD Application with Certificate Authentication

Calling the Microsoft Graph, SharePoint Online, or other resource via an Azure AD Application is a fairly straightforward process when you use client ID + secret for the authentication mechanism.  You can think of the client ID and secret as a username and password for authentication.  Note that anyone who has that client ID + secret can log in as that Azure AD App and perform the actions that it has been granted.  In an enterprise or secure environment certificate authentication is a more secure authentication option as it requires physically having the certificate which will only be deployed in a private fashion.  In this post I’ll walk through how to deploy and leverage the necessary components to accomplish this.  This example is part of a larger Azure Functions sample that I plan to release at a later date but the snippets below could be adapted for other hosting platforms.

 

Components Needed

  • Certificate (self-signed or generated from a PKI-type infrastructure)
  • Azure AD Application (using V1 in this example) with Microsoft Graph OAuth permissions
  • Azure Function

 

Solution Overview

  1. Create certificate (self-signed in this example)
  2. Create Azure function
  3. Create Azure AD application registration
  4. Add certificate metadata to Azure AD application
  5. Deploy certificate to Azure Function certificate store
  6. Authenticate to Azure AD application using certificate

 

1) Create Certificate

If you are on Windows 8+ there is a PowerShell commandlet to create self-signed certificates easily.  If not you’ll need to leverage MakeCert.exe or another certificate generating mechanism (ex. New-SelfSignedCertificateEx, documentation).  Here is a sample of the PowerShell option.

 

# process for Windows 8+ type OS
$ssc = New-SelfSignedCertificate -CertStoreLocation $CertificateStoreLocation -Provider $ProviderName `
    -Subject "$CertificateSubject" -KeyDescription "$CertificateDescription" `
    -NotBefore (Get-Date).AddDays(-1) -NotAfter (Get-Date).AddYears($CertificateNotAfterYears) `
    -DnsName $CertificateDNSName -KeyExportPolicy Exportable

# Export cert to PFX - uploaded to Azure App Service
Export-PfxCertificate -cert cert:\CurrentUser\My\$($ssc.Thumbprint) -FilePath $certificatePFXPath -Password $CertificatePassword -Force

# Export certificate - imported into the Service Principal
Export-Certificate -Cert cert:\CurrentUser\My\$($ssc.Thumbprint) -FilePath $certificateCRTPath -Force

 

2) Create Azure Function

You can create an Azure Function from the Azure Portal (reference), Azure CLI (reference), or through tools / extensions built into Visual Studio 2017 (reference) / Visual Studio Code.

 

3-4) Create Azure AD Application and Add Certificate to Azure AD Application

Here is a sample for creating an Azure AD application using Azure PowerShell.  In this example the certificate is added (-KeyCredentials) to the Azure AD application at time of creation, but it could also be added after the fact through the Azure Portal or PowerShell as well.

 

# prepare certificate for usage with creating AAD app
$KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable, `
    [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet, `
    [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
$certFile = Get-ChildItem -Path $CertificatePFXPath
$x509 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$x509.Import($certFile.FullName, $CertificatePassword, $KeyStorageFlags)
$certValueRaw = $x509.GetRawCertData()

$validFrom = $x509.NotBefore
$validTo = $x509.NotAfter
$keyId = [guid]::NewGuid()

$keyCredential = New-Object -TypeName "Microsoft.Open.AzureAD.Model.KeyCredential"
$keyCredential.StartDate = $validFrom
$keyCredential.EndDate= $validTo
$keyCredential.KeyId = $keyId
$keyCredential.Type = "AsymmetricX509Cert"
$keyCredential.Usage = "Verify"
$keyCredential.Value = $certValueRaw

$aadApp = New-AzureADApplication -DisplayName $AADAppName -Homepage $HomePage -ReplyUrls $ReplyUrls `
    -IdentifierUris $IdentifierUri -KeyCredentials $keyCredential

 

5) Deploy certificate to Azure Function

While there is a native way to upload a certificate to an Azure App Service via the Azure CLI and the Azure Portal there is not a direct way via PowerShell.  I was able to mimic an option with PowerShell by adding an SSL binding with a certificate and then immediately removing the SSL binding while not deleting the certificate (“-DeleteCertificate $false”).  Below are examples for both options.

Note: In both examples below the password will be entered as cleartext instead of using a SecureString or other encrypted mechanism. This could pose a security risk but I haven’t found an alternative as of yet.

PowerShell

$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($certificatePassword)
$ClearTextPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

New-AzureRmWebAppSSLBinding -ResourceGroupName $resourceGroupName -WebAppName $webAppName -Name $webAppDNSName -CertificateFilePath (Get-ChildItem .\$certificatePFXPath) -CertificatePassword $ClearTextPassword
Remove-AzureRmWebAppSSLBinding -ResourceGroupName $resourceGroupName -WebAppName $webAppName -Name $webAppDNSName -DeleteCertificate $false -Confirm:$false -Force

 

Azure CLI

az webapp config ssl upload --certificate-file "(certPath)" --certificate-password "(certPassword)" --name "(certName)" --resource-group "(resourceGroup)"

6) Authenticate to Azure AD application using certificate

The Azure Function code can authenticate to the Azure AD application using the certificate that was deployed in step 5.  Below is a sample of the code used to retrieve the certificate.  Since Azure Functions can be run locally or in Azure this will work locally if the certificate has been deployed to the certificate store or in Azure when deployed to the App Service.


public static X509Certificate2 GetCertificate(string thumbprint)
{
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
    store.Open(OpenFlags.ReadOnly);

    var col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
    if (col == null || col.Count == 0)
    {
        return null;
    }
    return col[0];
}
finally
{
    store.Close();
}
}

 

Below is a sample of using the certificate to authenticate to SharePoint Online, but this could easily point to a different resource such as Microsoft Graph, Exchange Online, etc.

var url = Environment.GetEnvironmentVariable("tenantRootUrl");
var thumbprint = Environment.GetEnvironmentVariable("certificateThumbprint");
var resourceUri = Environment.GetEnvironmentVariable("resourceUri");
var authorityUri = Environment.GetEnvironmentVariable("authorityUri");
var clientId = Environment.GetEnvironmentVariable("clientId");
var ac = new AuthenticationContext(authorityUri, false);
var cert = GetCertificate(thumbprint);  //this is the utility method called out above
ClientAssertionCertificate cac = new ClientAssertionCertificate(clientId, cert);
var authResult = ac.AcquireTokenAsync(resourceUri, cac).Result;

#next section makes calls to SharePoint Online but could easily be to another resource
using (ClientContext cc = new ClientContext(url))
{
    cc.ExecutingWebRequest += (s, e) =>
    {
        e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + authResult.AccessToken;
    };

    #make calls through the client context object
    #…
}

 

Conclusion

This process is part of a much larger solution used to make authenticated calls to an Azure AD application from an Azure Function.  I am working on publishing that solution as a sample for others to reference.  I am hopeful that I’ll have something available within a month.  For the time being feel free to reference the above steps and code snippets for use in your own project.  Feel free to contact me or leave a comment if you have questions or feedback.

 

-Frog Out

Calling Azure AD Secured Azure Function Externally From JavaScript

My customer recently had a need to securely call an HTTP trigger on an Azure Function remotely from an arbitrary client web application.  In this scenario securely meant ensuring that the user has logged into Azure Active Directory (AAD), but any number of authentication providers could be used.  The SharePoint Patterns and Practices (PnP) team had posted a video (SharePoint PnP Webcast – Calling external APIs securely from SharePoint Framework) that used the SharePoint Framework but my team needed to do this from vanilla JavaScript.  Many thanks to the PnP team and my peer Srinivas Varukala for their inspiration and code samples.

Overview

The key components to this solution involve the following:

  • Azure AD app registration (used to enforce authentication on Azure Function)
  • Azure Function configured to enforce Azure AD authentication
  • Client web application with JavaScript code to call the Azure Function

Azure Function

In the Azure Portal create a new Azure Function.  Choose an HTTP Trigger and use the language of choice (I’m using C# script in this example).  The Azure Function will validate if a claims principal exists on the incoming request and then output to the logs the name of the user if authenticated.

<Update 2018-05-02>

Note: the Azure portal currently does not support the headers required for CORS (cross-origin resource sharing) requests that contain credentials.  Feedback (source) has been provided to the Azure App Service team to support this but was declined.  As such the manual processing of CORS  requests is not supported at this time.  You will need to determine if this workaround works for you or not.

</Update 2018-05-02>

using System.Net;
using System.Security.Claims;
using System.Threading;
public static HttpResponseMessage Run(HttpRequestMessage req, TraceWriter log)
{
  log.Info("C# HTTP trigger function processed a request.");
// check for authenticated user on incoming request
  if (!ClaimsPrincipal.Current.Identity.IsAuthenticated)
  {
    log.Info("Claims: Not authenticated");
  }
  else
  {
    log.Info("Claims: Authenticated as " + ClaimsPrincipal.Current.Identity.Name);
  }

  var resp = req.CreateResponse(HttpStatusCode.OK, "Hello there " + ClaimsPrincipal.Current.Identity.Name);

  // manually process CORS request
  if (req.Headers.Contains("Origin"))
  {
    var origin = req.Headers.GetValues("origin").FirstOrDefault();
    resp.Headers.Add("Access-Control-Allow-Credentials", "true");
    resp.Headers.Add("Access-Control-Allow-Origin", origin);
    resp.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    resp.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Set-Cookie");
  }
  return resp;
}

Azure AD App

In order to enforce Azure AD authentication on the Azure Function an Azure AD app registration needs to be created.  Log into the Azure AD admin portal.  Under Azure Active Directory –> App Registrations create a new app registration.

SecureCallAFFromJS1

Take note of the Application ID (also known as client ID) for the application created.  This will be used later in the Azure App Service authentication / authorization configuration.

SecureCallAFFromJS2

The default permissions for the Azure AD app registration (delegated: sign in and read user profile) will be sufficient.

SecureCallAFFromJS3

SecureCallAFFromJS4

Enforce authentication

Return to the Azure Function and navigate to the Platform features –> Authentication / Authorization screen.  Turn App Service Authentication to On, set “Action to take…” to “Log in with Azure Active Directory”, then click the Azure Active Directory authentication provider to configure it as follows.

SecureCallAFFromJS5

Fill in the Application ID / Client ID from the previously created Azure AD app registration.  Specify the IssuerUrl of the Azure AD domain (typically https://login.microsoftonline.com/TENANT_NAME_GOES_HERE.onmicrosoft.com).

SecureCallAFFromJS6

Remove CORS configuration

As noted previously, the Azure portal currently (as of writing May 1, 2018) does not support Azure App Service processing CORS requests that contain credentials.  As such removing all domains from the CORS configuration in Azure Portal is unsupported.  Please validate if this workaround works for you or not.

SecureCallAFFromJS7

Client code

In this example I started with a .Net Framework MVC project from Visual Studio 2017 v15.6.7 but the code could be hosted on any page with HTML, JavaScript, and a logged in user to Azure AD.  Note that the MVC project allows enforcing Azure AD authentication which is what I was most interested in.

SecureCallAFFromJS8

SecureCallAFFromJS9
HTML snippet to include inside of an IFRAME element with source pointing to root of Azure Function.

SecureCallAFFromJS13

JavaScript snippet to call HTTP trigger of Azure Function.

Note that in a production scenario you would want to ensure that the IFRAME has loaded fully (and thus authentication cookie set) prior to the Azure Function being called.

  $(document).ready(function () {
    $("#btnExternalCall").click(function (e) {

      var serviceURL = "https://NAME_OF_AZURE_FUNCTION_GOES_HERE.azurewebsites.net/api/HttpTriggerCSharp1";

      $.ajax({
        url: serviceURL,
        type: "GET",
        xhrFields: {
          withCredentials: true
        },
        crossDomain: true,
        success: function (data) {
          alert("Success: " + data);
        },
        error: function (ex) {
          alert("Failure getting user token");
      }
    });
  });
});

Testing

When all has been configured you can test scenario.  If you enter F12 developer tools from your browser of choice you should see the authentication cookie for both the client web application as well as the Azure Function domains.

SecureCallAFFromJS10

After issuing the call to the HTTP Trigger on the Azure Function you should see that the call was indeed authenticated and the ClaimsPrincipal is returned.

SecureCallAFFromJS11SecureCallAFFromJS12

Conclusion

This is a very powerful capability being able to ensure that an HTTP trigger on an Azure Function only allows authenticated users to call the endpoint.  Hopefully this post helps others who have a similar need.  Please leave and questions or feedback in the comments below.

-Frog Out