Step by Step securing Applications with AAD B2C and EasyAuth

Today I show you step by step how you can use Azure Active Directory Business to Consumer (AAD B2C) to secure your backend site/services.

For that reason I create a service backend and two web applications:

  • REST Backend-Service (Web-API) secured by AADB2C
  • Single Page Application
  • ASP.NET Application

Both applications will be able to access the web-api after the user is authenticated. The following diagram illustrates our example:

Create the Infrastructure

First of all we will create our required infrastructure:

  • AAD B2C Instance in Azure
  • 3 Azure Web Apps

Setup the Azure Web Apps

  1. Login to https://portal.azure.com
  2. Create a new resource group (f.e. "b2ctest")
  3. Add 3 Web Apps to this resource group
SPA-App Web-API ASP.NET Core App
Name b2cspaspec* b2ctestspecapi* b2ctestspec*
Runtime Node 12 (LTS) .NET Core 3.1 .NET Core 3.1
Plan YourFreePlan-1 YourFreePlan-1 YourFreePlan-1
https://b2cspaspec.azurewebsites.net/ https://b2ctestspecapi.azurewebsites.net/ https://b2ctestspec.azurewebsites.net/

*) These names are already taken, choose unique names!

  1. Configure Git-Deployment for the SPA-App
    • Select the web app in the azure portal
    • Under Deployment** – Deployment Center
      • Select CI / Local Git and click Continue
      • Select "App Service build service" and click Continue
      • Finish
Git-Clone Url https://b2cspaspec.scm.azurewebsites.net:443/b2cspaspec.git
Username $b2cspaspec
Password av********cs

Copy the Git Clone URL and store the git credentials

Setup Azure AAD B2C Tenant

Next we will create a new Azure Active Directory B2C Tenant. You can skip that step if you already have one.

  1. Login to https://portal.azure.com
  2. Create a new ressource group for your AAD B2C
  3. Add an Azure Active Directory B2C instance (Settings should be obvious)
    • Organization Name
    • Initial Domain name
    • Country/Region
  4. To access your AAD B2C Tenant you need to switch to that tenant like you would switch to another AAD tenant. (Upper right corner)

Create User Flows in AAD B2C

Navigate to the AAD B2C Overview page

Select New user flow under Policies / UserFlows to select a predefined user flow. We select Sign up and sign in in the recommended version. This will allow a potential user to sign up and in, or an existing user to sign in.

Configure the flow as so:

  • Name: B2C_1_ (f.e. signupin ==> B2C_1_signupin)
  • Identity Provider: Select "Email signup"
  • MFA: SMS or phone
  • MFA Enforcement: Conditional
  • Check Conditional Access option
  • Select additional attributes to be collected

Click CREATE to create the flow. Under User Flows select your flow again and click on Properties as we want to modify the password complexity.

I choose the following settings, but you can obviously choose different:

  • Complexity: Custom
  • Character set: All
  • Minimum length: 16
  • Maximum length: 80
  • Character classes required: None

Create Application registrations in AAD B2C

Each of our services (2 webapps and 1 webapi) need to be secured and therefore represented as AAD applications in our B2C tenant.

Again navigate to the AAD B2C Overview page and select App registrations

Register AAD-App Web-API

  • Name: b2ctestspecapi

  • Supported account type: Accounts in any identity provider or organizational directory (for authenticating users with user flows)

  • Redirect URI: Webhttps://b2ctestspecapi.azurewebsites.net/.auth/login/aad/callback (Enter you WebApp URI there and append /.auth/login/aad/callback which will be the path EasyAuth of Azure App Service will require!)

  • Under Overview note the Application (Client) ID (af9c********0f66 in my case)

  • Under Manage / Authentication / Implicit Grant

    • Check "Access Tokens"
    • Check "ID Tokens"
  • Under Manage / Certificates & secrets / Client Secrets

    • Create a new secret and keep it (-Ox********** in my case)
  • Under Manage / API permissions add following permissions (Do not forget to GRANT CONSENT afterwards)

    • offline_access (Delegated)
    • openid (Delegated)
    • Grant admin consent for your tenant
  • Under Manage / Expose an API

Register AAD-App ASP.NET Core App

  • Name: b2ctestspec

  • Supported account type: Accounts in any identity provider or organizational directory (for authenticating users with user flows)

  • Redirect URI: Webhttps://b2ctestspec.azurewebsites.net/.auth/login/aad/callback (Enter you WebApp URI there and append /.auth/login/aad/callback which will be the path EasyAuth of Azure App Service will require!)

  • Under Overview note the Application (Client) ID (cf6d********d2ee in my case)

  • Under Manage / Authentication / Implicit Grant

    • Check "Access Tokens"
    • Check "ID Tokens"
  • Under Manage / Certificates & secrets / Client Secrets

    • Create a new secret and keep it (.dSz********** in my case)
  • Under Manage / API permissions add following permissions if not present(Do not forget to GRANT CONSENT afterwards)

    • offline_access (Delegated)
    • openid (Delegated)
    • From My Apis select b2ctestspecapi (app we created earlier) and add both permissions:
      • Spec.Read
      • Spec.Write
    • Grant admin consent for your tenant

Register AAD-App SPA-App

  • Name: spademo

  • Supported account type: Accounts in any identity provider or organizational directory (for authenticating users with user flows)

  • Redirect URI: Single-page application SPAhttps://b2cspaspec.azurewebsites.net/ (We will use MSAL here)

  • Under Overview note the Application (Client) ID (9192********fe5 in my case)

  • Under Manage / Authentication / Platform Configurations

  • Under Manage / Authentication / Implicit Grant

    • Check "Access Tokens"
    • Check "ID Tokens"
  • Under Manage / API permissions add following permissions if not present(Do not forget to GRANT CONSENT afterwards)

    • offline_access (Delegated)
    • openid (Delegated)
    • From My Apis select b2ctestspecapi (app we created earlier) and add both permissions:
      • Spec.Read
      • Spec.Write
    • Grant admin consent for your tenant

Test User Flow and create Test-User

Again navigate to the AAD B2C Overview page and select B2C_1_signupin policy under Policies/User Flows. Click on the Run user flow button and select the spademo application. As reply url choose https://jwt.ms so we can examine the result. Select no access token ressources now. Click Run user flow

You should see a SignIn/SignUp Dialog – Create a new user with one of your EMail-Addresses. Once you hit create you should be redirected to https://jwt.ms where you can examine you Auth-Token.

Secure Web-API and ASP.NET Core App with Azure App Easy Auth

Since the best security code is the code you never write, we want to make use of Azure Apps Easy Auth service to protect our ASP.NET Core App and Service.

For this switch back into your regular tenant (where the Azure Apps are located). To view all App Services in your tenant click this link.

Secure B2CTestSpecAPI AppService

First we configure EasyAuth for our API app service. Then we create a simple api application that we deploy there.

Navigate to your b2ctestspecapi App Service.

Under Settings – Authentication/Authorization enable App Service Authentication. Also enable the Token Store (at the end of the page). Select Login with Azure Active Directory as action. This will ensure that no unauthenticated call is reaching your application.

Select Azure Active Directory from the list of Authentication Providers Under the Managment Mode click on Advanced

  • ClientId: af9c************0f66
  • Issuer URL: https://yourtenantname.b2clogin.com/yourtenantname.onmicrosoft.com/B2C_1_signupin/v2.0/.well-known/openid-configuration (We had this url from the Endpoints if you remember)
  • Client Secret: .-OX*************

Create a simple ASP.NET Core App AADDemoAPI in Visual Studio which automatically will have the WeatherForeCast API implemented. Deploy this service to this Azure App Service.

Test the authentication by navigating to https://b2ctestspecapi.azurewebsites.net which should require a login. Once you have logged in you should be able to examine the tokens by accessing: https://b2ctestspecapi.azurewebsites.net/.auth/me

Secure B2CTestSpec AppService

Navigate to your b2ctestspec App Service.

Under Settings – Authentication/Authorization enable App Service Authentication. Also enable the Token Store (at the end of the page). Select Login with Azure Active Directory as action. This will ensure that no unauthenticated call is reaching your application. If you want to serve some pages without authenticating choose the other option. In this case however you need to check in you code if you are authenticated or not! If not you have to make sure to redirect the user to the login page.

Select Azure Active Directory from the list of Authentication Providers Under the Managment Mode click on Advanced

  • ClientId: cf6d************a2ee
  • Issuer URL: https://yourtenantname.b2clogin.com/yourtenantname.onmicrosoft.com/B2C_1_signupin/v2.0/.well-known/openid-configuration (We had this url from the Endpoints if you remember)
  • Client Secret: .dSz*************
  • Allowed Token Audiences:

This should do the trick right? Well as long as we want only to authenticate this will work. But if we also want the access token to access our WebAPI we need to do more.

Right no configuration in the UI where you can specify the required permission scopes. To request the scopes you have to redirect users to this url:

There is however a workaround to this i stumbled upon in the documentation. Using the mangement API you can set the default scopes for EasyAuth. If you want to use an UI to do that use https://ressources.azure.com and navigate to subscriptions/yoursubscription/resourceGroups/yourResourceGroup/providers/Microsoft.Web/sites/yourSite/config/authsettings. Under the entry allowedaudiences add:

  • resource: Client ID of the WebAPI you want to call
  • scope: add webservices scopes to openid, offline access
"additionalLoginParams": [
	  "resource=af9c************0f66",
	  "scope=openid offline_access https://spectologicb2crtm.onmicrosoft.com/specapi/Spec.Read https://spectologicb2crtm.onmicrosoft.com/specapi/Spec.Write"
	]

Let’s now implement an ASP.NET Core Application that we can deploy to this Azure App Service and will call the API webservice we created earlier.

In Visual Studio 2019 create a new ASP.NET Core MVC app named AADDemoWF and implement the Index-Method of HomeController.cs

public async Task Index()
{
    ViewData["X-MS-TOKEN-AAD-ID-TOKEN"] = Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];
    ViewData["X-MS-TOKEN-AAD-ACCESS-TOKEN"] = Request.Headers["X-MS-TOKEN-AAD-ACCESS-TOKEN"];
    ViewData["X-MS-TOKEN-AAD-EXPIRES-ON"] = Request.Headers["X-MS-TOKEN-AAD-EXPIRES-ON"];
    ViewData["X-MS-TOKEN-AAD-REFRESH-TOKEN"] = Request.Headers["X-MS-TOKEN-AAD-REFRESH-TOKEN"];
 
    string accessToken = Request.Headers["X-MS-TOKEN-AAD-ACCESS-TOKEN"];
    try
    {
        string url = "https://b2ctestspecapi.azurewebsites.net/weatherforecast";
        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Add("Authorization", $"Bearer {accessToken}");
        var response = await _client.SendAsync(request);
        ViewData["SERVICERESP"] = await response.Content.ReadAsStringAsync();
 
    }
    catch (Exception ex)
    {
        ViewData["SERVICERESP"] = ex.ToString();
    }
    return View();
}

Also adapt the index.cshtml page to visualize the tokens and the service response.

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Auth Tokens</h1>
<div>X-MS-TOKEN-AAD-ID-TOKEN</div><br />
<div>X-MS-TOKEN-AAD-ACCESS-TOKEN</div><br />
<div>X-MS-TOKEN-AAD-EXPIRES-ON</div><br />
<div>X-MS-TOKEN-AAD-REFRESH-TOKEN</div><br />
<div>SERVICERESP</div><br />

<a href="https://b2ctestspec.azurewebsites.net/.auth/login/aad?p=&post_login_redict_uri=/&scope=openid+offline_access+https%3A%2F%2Fspectologicb2crtm.onmicrosoft.com%2Fspecapi%2FSpec.Read&redirect_uri=https%3A%2F%2Fb2ctestspec.azurewebsites.net%2F">Explicit</a>
</div>

Deploy this application to the App Service and you should be able to access this site after logging in through AAD B2

Secure the SPA Application

In this case I am using a sample provided by microsoft that we will adapt to our requirements. Open a terminal and clone the sample of microsoft.

First we will change app/authConfig.js. If you do not request all required resource permission scopes during login the silet token aquiring will fail and fall back to a authentication that will show a brief popup. Since the user is logged on already this will just result in a brief flicker. To avoid this we add all base scopes in the initial login.

// Config object to be passed to Msal on creation.
// For a full list of msal.js configuration parameters, 
// visit https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md

const msalConfig = {
    auth: {
        clientId: "9192**************0fe5",
        authority: "https://spectologicb2crtm.b2clogin.com/spectologicb2crtm.onmicrosoft.com/B2C_1_signupin",
        knownAuthorities: [ 'spectologicb2crtm.b2clogin.com' ],
        redirectUri: "https://b2cspaspec.azurewebsites.net/",
    },
    ... 

// Add here the scopes that you would like the user to consent during sign-in
const loginRequest = {
    scopes: ["openid","offline_access","https://spectologicb2crtm.onmicrosoft.com/specapi/Spec.Read"]
};
 
// Add here the scopes to request when obtaining an access token for MS Graph API
const tokenRequest = {
    scopes: ["User.Read", "Mail.Read"],
    forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
};
 
const weatherTokenRequest = {
    scopes: ["https://spectologicb2crtm.onmicrosoft.com/specapi/Spec.Read"],
    forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
};

Change app/graphConfig.js and add weatherConfig there:

// Add here the endpoints for MS Graph API services you would like to use.
const graphConfig = {
    graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
    graphMailEndpoint: "https://graph.microsoft.com/v1.0/me/messages",
};
...
// Add here the endpoints for Weather services you would like to use.
const weatherConfig = {
    weatherEndpoint: "https://b2ctestspecapi.azurewebsites.net/weatherforecast"
};

Add a function callWeatherAPI to app/graph.js. The code just makes a request to an given endpoint with a bearertoken.

function callWeatherAPI(endpoint, token, callback) {
    console.log(endpoint);
    console.log(token);
    const headers = new Headers();
    const bearer = `Bearer ${token}`;
 
    headers.append("Authorization", bearer);
 
    const options = {
        method: "GET",
        headers: headers
    };
 
    console.log('request made to Weather API at: ' + new Date().toString());
 
    fetch(endpoint, options)
        .then(response => response.json())
        .then(response => callback(response, endpoint))
        .catch(error => console.log(error));
}

Change app/authPopup.js and adapt the seeProfile-Method that is called once a user clicks on the Profile-Button

...
function seeProfile() {
    getTokenPopup(weatherTokenRequest).then(response => {
        callWeatherAPI(
            weatherConfig.weatherEndpoint, 
            response.accessToken, 
            updateUI);
    }).catch(error => {
        console.error(error);
    });
}
...

The upper method will call updateUI-function after a successful call to the webservice. We change the implementation in app/ui.js

...
function updateUI(data, endpoint) {
    console.log('Graph API responded at: ' + new Date().toString());
 
    if (endpoint === weatherConfig.weatherEndpoint) {
        const dataElement = document.createElement('p');
        dataElement.innerHTML = "<strong>Data: </strong>" + JSON.stringify(data);
        profileDiv.appendChild(dataElement);
    } else if (endpoint === graphConfig.graphMeEndpoint) {
        const title = document.createElement('p');
...

We are almost done now. We need to change Server.js to serve the right port for Azure App Service:

// app.listen(port); Replace this line with the next
app.listen(process.env.PORT||port);

Check in all changes to master in this git repository!

Now deploy our git repository to our NODE Azure App Service by using the credentials we created earlier.

Now you can try your service at https://b2cspaspec.azurewebsites.net/

That’s it for today! Have a great day!

AndiP

Accessing AAD OAuth2 protected API from PHP

Overview

What are we building today?

We do have a Web APP API running in an Azure App Service that is protected with Azure Active Directory (AAD). We want to access this webservice from a PHP website. In our setup the php website runs in a local docker container. The first and foremost goal is to show how we can get hands on the access token from our PHP application.

  1. Request the Index.php through http://localhost:90
  2. Use the OAuth2 library thenetworg/oauth2-azure to request an access token
  3. Authenticate against the app phpDemo in AAD and request access to ressource weatherapi
  4. Return the access token
  5. Return access token. Here you could call the web service with the returned bearer token
  6. Render the token on the website
  7. Use Postman to call the protected webservice

In the following sections contain a step by step guidance how to implement this scenario.

Creating the API-Application

Open Visual Studio 2019 and create a new „ASP.NET Core Web App“

  • Name: weatherapi
  • Plattform: .NET Core / ASP.NET Core 3.1
  • Template: API

In the solution explorer right click the project – Publish…

  • Azure
  • Azure App Service (Windows)
  • Select your subscription
  • + Create a new Azure App Service …
    • Name: weatherapi (select a unique name)
    • Subscription: Your Subscription
    • Resource Group: New… (phpdemo)
    • Hosting Plan: New… (testplan)

    • Click Create to create the Azure Resources
  • Select the web app and click „Finish“
  • Click Publish to publish the WebApp

Now you should be able to call your webservice under following URL: https://weatherapi.azurewebsites.net/weatherforecast

Create AAD App for ASP.NET API with weather.read

"appRoles": [
{
"allowedMemberTypes": ["Application" ],
"description": "Weather API",
"displayName": "Weather API",
"id" : "4B*************08",
"isEnabled": true,
"lang" : null,
"origin": "Application",
"value": "weather.read"
}
],

Securing the API Application with Easy Auth

  • Navigate to Azure App Services
  • Select your weatherapi Application
    • Under Settings click Authentication / Authorization
    • Turn App Service Authentication ON
      • In the drop down select „Log in with Azure Active Directory“
      • Click „Azure Active Directory“
      • Select Advanced
      • Copy ClientID to Client ID field
      • Copy „OpenID Connect metadata document“ to Issuer Url
      • Click OK
    • Save

Take care with the Issuer URL, because your application might break if you use JWT-Tokens with a „wrong“ issuer. JWT-Tokens with Ver 1.0 have a different Issuer than JWT-Tokens Ver 2.0. 

Open the  OpenID Connect Metadata Url in a browser and locate the „issuer“ element. It will be set to:

If you use V1 Tokens you need to set Issue in Easy Auth to

The „iss“ element in the tokens must match the issuer configured in EasyAuth! Read more on Access Tokens V1/V2 here!

Create AAD App for PHP Application

  • Open https://portal.azure.com
  • Navigate to Azure Active Directory / App registrations
  • + New registration
    • Name: phpdemo
    • Account Types: Single Tenant
    • Redirect URI: Web – http://localhost:90
    • Register
  • Write down ClientID. In my case it is: c9e*****9ea
  • Click on the button „Endpoints“
  • Click on Manage – Certificates & secrets
    • + New client secret
      • Name: mysecret
      • Expiry: 2 Years
      • Copy down the Secret (we need that later). In my case its: AKX**********
  • Click on Manager – API Permissions
    • + Add a permission
      • Select „My APIs“-Tab
      • Select „weatherapi“
      • Select „Application permissions“
      • Select „weather.read“
      • Add Permissions
    • Note the orange exclamation sign! Use „Grant admin consent for…“ button next to „+ Add permission“ and confirm with Yes.

Creating the PHP Application

Since I did not want to install all PHP stuff on my machine I decided to develop my example in a docker container. Thanks to the TruthSeekers who wrote this excellent post on how to do exactly that. You can clone the source code here.

After I cloned the git repository I had to adopt my docker-compose.yml file slightly since port 80 / 8080 are already consumed on my machine.

... 
ports: 
- 90:80
... 
ports:
- 9090:8080 
... 
Then I simply ran
  • docker-compose up -d

which brought up all containers, including SQL, which we won’t be needing now. Since we need some OAuth-Libraries for PHP, I decided to use the package manager tool composer.  

I did a quick „docker container list“ to figure out the php container.

Then with

  • docker exec -it php-docker-simple_php_1 bash

I connect to the bash of the running container.

Execute the following statements to install composer:

apt-get update
apt-get install zip unzip
Cd ~
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '795f976fe0ebd8b75f26a6dd68f78fd3453ce79f32ecb33e7fd087d39bfeb978342fb73ac986cd4f54edd0dc902601dc') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

As OAuth2 library I want to use „thenetworg/oauth2-azure“ which can be found here.
It is based on the OAuth2 Library „thephpleague/oauth2-client“ which can be found here.

Then install this package to the sample which resides in /var/www/html:

 
cd /var/www/html
php ~/composer.phar require thenetworg/oauth2-azure

Exit to the terminal and start VS-Code in the local directory or any other editor of your choice

For our sample we need following information which we collected earlier:

  • Client ID of the phpdemo app
  • ClientSecret of the phpdemo app
  • redirectUri of phpdemo app
  • aad tenant id
  • OAuth 2.0 Authorize Url
  • OAuth 2.0 Token Url
  • Client ID of the weatherapi app

We need all these values and add them in the following code fragment which initializes the oauth provider for Azure AAD:

$provider = new TheNetworg\OAuth2\Client\Provider\Azure([
'clientId' => 'c9e*****9ea',
'clientSecret' => 'AKX********',
'redirectUri' => 'http://localhost:90',
'tenant' => 'e2*******3d',
'urlAuthorize' => 'https://login.microsoftonline.com/e2*******3d/oauth2/v2.0/authorize',
'urlAccessToken' => 'https://login.microsoftonline.com/e2*******3d/oauth2/v2.0/token',
'urlAPI' => 'b37*******b02',
'scope' => 'b37*******b02/.default'
]);
$provider->defaultEndPointVersion = TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;

We can now retrieve the access token with the following code:

$accessToken = $provider->getAccessToken('client_credentials', [
'scope'=> $provider->scope
]);
Here is the complete code of src/index.php to get and display the access token that we can then use to make a request to our api service:

<?php
echo "OAuth Sample<br/>";
/* Make sure we include necessary libraries */
require_once(__DIR__."/vendor/autoload.php");
/* Initialize provider */
$provider = new TheNetworg\OAuth2\Client\Provider\Azure([
    'clientId' => 'c9e*****9ea',
    'clientSecret' => 'AKX********',
    'redirectUri' => 'http://localhost:90',
    'tenant' => 'e2*******3d',
    'urlAuthorize' => 'https://login.microsoftonline.com/e2*******3d/oauth2/v2.0/authorize',
    'urlAccessToken' => 'https://login.microsoftonline.com/e2*******3d/oauth2/v2.0/token',
    'urlAPI' => 'b37*******b02',
    'scope' => 'b37*******b02/.default'
]);
$provider->defaultEndPointVersion = TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;

try {
    $accessToken = $provider->getAccessToken('client_credentials', [
        'scope' => $provider->scope
    ]);
} catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
    // Failed to get the access token
    exit($e->getMessage());
}
echo "Access-Token:";
echo $accessToken;

Now we can call http://localhost:90 on our machine and retrieve the JWT Bearer Token.
Open Postman and make a get request to:

In the Authorization TAB select type: „Bearer Token“ and paste the access token to the token field.
If you hit Send now, you will retrieve the results of the weatherforecast.

That’s it. Thanks
Andreas