Securing external SaaS APIs with Azure API Management (GAB2022)

Thank you for attending my session at the Global Azure Bootcamp 2022 Austria. This blog post contains a link to my github repository, where I published the slides along with all my policy and api definitions. You also can find the steps I used to create my certificate chain with open ssl. To do that I used this guide to create a self-signed certificate chain with openssl here.

The recording of this session will be published soon on YouTube (Link will be updated here).

Session abstract

Some vendors have very limited abilities to restrict the access to their APIs to a minimum. Especially when highly sensitive data is stored with the SaaS-Provider (e.g. CRM Solutions) it can be a challenge from a security perspective to outsource an integration to a 3rd party. Of course you have contracts, liabilities and other things in place, but it does not f.e. prevent breaches at the 3rd Party provider. While it is necessary that the integration partner retrieves some customer data, they should not be able to retrieve all data. Seeking for an easy way to secure this we found Azure API Management to be a good fit. In this session I will show you how we used Azure API Management to secure the APIs with certificate authentication and make sure that only a subset of the API can be used.

Enjoy a great day

Nina

My favorite Visual Studio Extensions 2021

Here is my list of my most favorite Visual Studio Extensions! For my reference and as a guide to those seeking for such list!

Also if you like to learn some tips on tricks with Visual Studio I recommend joining my session on Visual Studio at ADC21 (German language).

Productivity Extensions

  • Code Maid (2019 / 2022)
    This awesome tool let’s you easily reorganize your source code and has many nice features like (Sync to Solution Explorer, Switch between related files (h/cpp, cshtml/cs, …)
  • Productivity Power Tools (2019 / 2022) – Options Dialog (2022)
    Still a valuable extension pack although it’s list of provided extensions is shrinking with each awesome Visual Studio release ❤️. My favorites are „Solution Error Visualizer“ and „Match Margin“ which I use frequently in Visual Studio.
    The options dialog let’s you tweak the settings in Visual Studio’s options!
  • Visual Studio Tweaks (2019 / 2022)
    A precious collection of extensions that make you life easier. Like the context menu special pasting JSON as classes, Code Cleanup on Format, and many more. Just one thing: If you notice that zoom with the mouse wheel stopped working: This is a feature and can be turned off under Options/Tweaks.
  • Code Cleanup on Save (2019 / 2022) – Format Document on Save (2019 / 2022)
    Allows to configure which Code-Cleanup Profile should be executed on saving a document
  • Git Pull (2019 / 2022)
  • Git Diff Margin (2019 / 2022)
    Visualizes very neatly what code you had changed directly in the editor ❤️ and let you undo it quickly without undoing the whole file. Has also nice indicators where code changes where done.
  • Insert Guid (2019 / 2022)
    For Guid-Nerds a must! Do not waste to much Guids! 😂
  • Switch Startup Projects (2019 / 2022)
    A very useful tool to switch the startup-projects very easily from a Drop-Down. Also supports multiple startup-projects. The configuration of those is a bit „complicated“ (json).
  • Preview JSON (2019 / 2022)
    Let’s you navigate through a Json-File easily
  • Add new file (2019 / 2022)
    SHIFT + F2 Shortcut to add new files and folders with a snap
  • Open in VSCode (2019 / 2022)
    Useful, if you want to quickly view a file as text (quicker than Open With,…)
  • Learn the Shortcut (2019)
    While a lot of shortcuts can be discovered by tool-tips and in the menu, this is a good helper for detecting more. It has not been ported to VS 2022 yet 😭 (I hope it will) – Helps to figure out

Build / Output Extensions

  • Clean Bin and Obj (2019 / 2022)
    Allows to cleanup and delete everything within Obj/Bin directories easily over the Build-Menu
  • Output Enhancer (2019 / 2022)
    Nicely colors your build/debug output for more clarity

Visual Studio Themes

  • Themes (2019 / 2022)
    While I love the look of dark themes (especially the NightOwl-Theme) I feel being more productive with light themes! Maybe it is my old eyes *laughs*.

Enjoy!

yours Nina

Register a HubSpot Integration App with Azure Functions

About Hubspot

Let’s start from the beginning. What is HubSpot? HubSpot is a Marketing-, Sales-, Service, CMS and Operations-Software. Learn more about HubSpot here.

What I like in particular with HubSpot is the integration possibilities it offers. It has a rich API and also the capability to call webHooks among other things.

However there are two ways to authenticate against the HubSpot API

  • API-Key
  • OAUTH

Hubspot itself disencourages you from using the API-Key other than for testing/developing purposes and given that an API-Key has no permissions attached to it and gives you control to almost everything in HubSpot you should never hand out the API-Key of a production system!

To develop applications you can register for a free developer account and sandbox for testing.

Concept and registration process of HubSpot Applications

HubSpot allows ISV’s (independent software vendors) to write integrations and offer those as „Hubspot-Integration-Apps“ to all customers of HubSpot. Therefore your integration always must be multi-tenant per se. Although there are some „workarounds“ to only allow access to certain HubSpot-Tenants as we see later.

As an ISV you register an „HubSpot-Application“ inside your Developer Account with HubSpot. Here you will gain access to a so called „Installation-Url“ that you can offer your customers to install you Applications in their HubSpot-Tenants. If you also publish your HubSpot-Application to the HubSpot-Store all customers will see this integration and can choose to install your application.

As you can see in the illustration below „your“ ISV Application from your Developer-Tenant is installed in the tenants of Customers A and B. Customer C has not installed „your“ ISV Application.

So how does the „Install-Process“ actually work conceptually? While registering your HubSpot-Application (this process is quite straight forward) you need to provide an End-Point that HubSpot can call once a customer decides to install the application to their tenant.

You also declare what permissions you require at a minimum so your integration works properly (For example: Access to Contacts)

If the user clicks on the installation url (which by the way can contain additional optional permissions) they have to confirm that they want to give access to the application.

Then the endpoint (ISV) simply will receive an „CODE“ that must be redeemed to get an Access Token from HubSpot. The base code template for an Azure Function App would look like this:

[FunctionName("RegisterHubSpotApp")]
public async Task<IActionResult> RegisterHubSpotApp(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
    HttpRequest req,
    ILogger log)
{
    try
    {
        var code = req.Query["code"].FirstOrDefault();
        if (!string.IsNullOrWhiteSpace(code))
        {
            // Redeem Code here!
        }
    }
    catch (Exception ex)
    {
        log.LogError(ex, ex.Message);
        return new BadRequestObjectResult(ex.Message);
    }
    return new BadRequestResult();
}

Unfortunately we still do not know which Customer Tenant did choose to install our application! The next step would be to acquire the Access-Token and Refresh-Token! Which still does not provide us with the information about the tenant!

Access-Token: This is the BEARER-Token you must use in the „Authorization„-Header when you make calls to the HubSpot-API. This will only be valid for a certain amount of time (see „ExpiresIn„-Field)

Refresh-Token: This is the token you must keep secure at all costs! Best to store it somewhere secure like Azure KeyVault. Having this token you can request new Access-Tokens that have expired! [SECURITY WARNING]

Said that to acquire the Access/Refresh-Token you need to provide following information that you can obtain from your registered HubSpot-Application:

  • ClientId
  • ClientSecret
  • RedirectUri (your Endpoint, where the Code was sent)

A simple implementation in an Azure Function might look like this:

public class OAuthTokenResponse
    {
        [JsonProperty(PropertyName = "token_type")]
        public string TokenType { get; set; }
        [JsonProperty(PropertyName = "refresh_token")]
        public string RefreshToken { get; set; }
        [JsonProperty(PropertyName = "access_token")]
        public string AccessToken { get; set; }
        [JsonProperty(PropertyName = "expires_in")]
        public int ExpiresIn { get; set; }
    }

private const string HUBSPOT_TOKEN_API = "https://api.hubapi.com/oauth/v1/token";

private async Task<OAuthTokenResponse> GetOAuthToken(string code)
{
    HttpClient client = _httpFactory.CreateClient();

    List<KeyValuePair<string, string>> data = new List<KeyValuePair<string, string>>()
        {
            new KeyValuePair<string, string>("grant_type","authorization_code"),
            new KeyValuePair<string, string>("client_id",_config["clientID"]),
            new KeyValuePair<string, string>("client_secret",_config["clientSecret"]),
            new KeyValuePair<string, string>("redirect_uri", _config["RedirectUri"]),
            new KeyValuePair<string, string>("code", code)
        };
    HttpRequestMessage httpRequest = new HttpRequestMessage
    {
        RequestUri = new Uri(HUBSPOT_TOKEN_API),
        Content = new FormUrlEncodedContent(data),
        Method = HttpMethod.Post
    };
    HttpResponseMessage httpResponse = await client.SendAsync(httpRequest);
    if (httpResponse.IsSuccessStatusCode)
    {
        var token = JsonConvert.DeserializeObject<OAuthTokenResponse>(await httpResponse.Content.ReadAsStringAsync());
        if (token != null && !string.IsNullOrWhiteSpace(token.RefreshToken))
        {
            return token;
        }
    }
    else
    {
        var response = await httpResponse.Content.ReadAsStringAsync();
    }
    return null;
}

So there is a lot of effort to be done and still we have no idea which Tenant has requested the install! Fortunately we can use the RefreshToken (keep in mind to keep it secure!) to retrieve some information about it.

The result of this API Call will look like this:

{
  "token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "user": "test@hubspot.com",
  "hub_domain": "demo.hubapi.com",
  "scopes": [
    "automation",
    "contacts",
    "oauth"
  ],
  "hub_id": 62515,
  "client_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
  "user_id": 123,
  "token_type": "refresh"
}

and gives us information about

  • the user who installed the application („user“, „user_id“)
  • the scopes we have available and can call apis for („scopes“)
  • the client-ID of our registered HubSpot-API (in case we have more than one integration app)
  • Finally the tenant-ID („hub_id“)

You might have noticed using Hubspot that the „hub_id“ is always a part of the URL. Example of an url to a contact:

An implementation in Azure Functions might look like this:

private async Task<OAuthRefreshTokenInfo> GetRefreshTokenInfo(string refreshToken)
{
    HttpClient client = _httpFactory.CreateClient();

    HttpResponseMessage tokenInfoResponse = await client.GetAsync(string.Format("https://api.hubapi.com/oauth/v1/refresh-tokens/{0}", refreshToken));
    if (tokenInfoResponse.IsSuccessStatusCode)
    {
        OAuthRefreshTokenInfo tokenInfo = JsonConvert.DeserializeObject<OAuthRefreshTokenInfo>(await tokenInfoResponse.Content.ReadAsStringAsync());
        if (tokenInfo.ClientId == _config["clientID"])
        {
            return tokenInfo;
        }
    }
    return null;
}

After we do have AccessToken, RefreshToken and Tenant-Information we need to store the Refresh-Token together with the Tenant-Information so our integration application can use this information to create an AccessToken on demand.

Make sure you cache the AccessToken securely to prevent unnecessary round-trips in your integration. And make sure the RefreshToken is stored securely (f.e. Azure KeyVault)!

Thanks and enjoy

Nina

Azure App Configuration presentation source code

On the 16th April 2021 I gave a presentation at Global Azure Bootcamp 2021 Austria. My talk about Azure App Configuration was presented in German (and you also can find the recording below).

You can download the source code of my presentation here at GitHub. Each finished Demo has been put in an extra branch.

It has been interesting to prepare and I created two specific implementation details I will follow up in future blog posts:

  • Use Azure Event Grid for pushing Azure App Configurations down to ASP.NET Core APP
  • Sticky Feature Session Manager for FeatureManagement

I hope you enjoyed my session!

AndiP

Cosmos DB RBAC-Access with Managed Identities

The public preview of role-based access control (RBAC) for the Azure Cosmos DB Core (SQL) API was announced yesterday at Microsoft Ignite. This does not only allow us to authenticate our requests with an Azure Active Directory identity based on roles, but also allows to audit the identities which accessed your data.

In this blog post I walk you through a complete example on how you can use Azure Cosmos DB with RBAC and managed identity. We will

  • Create following Azure Resources
    • Cosmos DB Account
    • Log Analytics Account
    • Azure Function App
  • Create a Azure Cosmos DB role with specific access permissions
  • Assign the CosmosDB role to a managed identity
  • Write an Azure Function App to access CosmosDB with managed identity

Create and configure Azure Resources

Open https://portal.azure.com and create a new resource group „cosmosrbac„.

Create a new Azure Cosmos DB Account in that resource group. For this sample I had been using the new serverless option. Use the DataExplorer (under Settings) to create a new Database „demodb“ and a Collection „democol„. I am using „/p“ as a generic partition key. This is especially useful, if you store different kind of documents in your collection which are partitioned differently. Also create a document with the „New document“ button since our permissions will be read only later.

{
"id":"001",
"p":"Test",
"First":"Andreas"
}

Now create a new Log Analytics Workspace and a Azure Function App (serverless) in the resource group „cosmosrbac„. I named those „cosmosrbaclog“ and „cosmosrbacfunc

Now lets configure the Azure Cosmos DB account so that all Dataplane Requests are audited in our Log Analytics Workspace. To do that select your Azure Cosmos DB resource, click on „Diagnostic settings“ in the section „Monitoring„. Add a new diagnostic setting where you just select „DataPlaneRequests“ and your created Log Analytics Workspace. I named that setting „SecurityLog“.

Diagnostic settings for Azure Cosmos DB account

In the portal select your Azure App Function and click on „Identity“ in the section „Settings„. Select „System assigned“ and turn the Status to „On„. Write down the given Object ID which represents the Object ID of the Service Principal in Azure AD. Read more about the managed identity types in the documentation here.

System assigned Identity for Azure Function App

Configure CosmosDB RBAC role and assign to identity

To create a new CosmosDB RBAC role we need to define which permissions the role consists of. This can be done by defining a JSON file. We will be creating a new role „MyReadOnlyRole“ with following permissions:

  • Microsoft.DocumentDB/databaseAccounts/readMetadata
  • Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read
  • Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery
  • Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed

You can find all available Cosmos DB permissions here.

In the Azure Portal open the cloud shell. You can find the cloud shell in the upper bar on the right side. I am using PowerShell version! If you use bash instead you might need to slightly adopt the statements below. To create and assign the role you need the latest Azure Cosmos DB az extension which you can install with this command:

az extension add --name cosmosdb-preview

Then start the nano editor and create a new file „roledefinition.json

nano roledefinition.json

Copy and paste the following json document and save the document by pressing CTRL+Q and selecting Y:

{
    "RoleName": "MyReadOnlyRole",
    "Type": "CustomRole",
    "AssignableScopes": ["/"],
    "Permissions": [{
        "DataActions": [
            "Microsoft.DocumentDB/databaseAccounts/readMetadata",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed"
        ]
    }]
}

Now create the defined role with the following statements. Make sure you replace the values of $resourceGroupName and $accountName with those you selected:

$resourceGroupName='cosmosrbac'
$accountName='cosmosrbac'
az cosmosdb sql role definition create --account-name $accountName --resource-group $resourceGroupName --body roledefinition.json

To later get a list of your defined roles you can issue this statement:

az cosmosdb sql role definition list --account-name $accountName --resource-group $resourceGroupName

Write down the value of „name“ of the new created role definition as we will need it in our next step.

Now we assign this new role to our previously created system managed identity. Replace the $readOnlyRoleDefinitionId value with the value of the role-name (see above) and replace the $principalId value with the Object ID we got earlier when we configured our Azure Function App:

$resourceGroupName='cosmosrbac'
$accountName='cosmosrbac'
$readOnlyRoleDefinitionId = 'name of role definition'
$principalId = 'object id of system managed identity'

az cosmosdb sql role assignment create --account-name $accountName --resource-group $resourceGroupName --scope "/" -p $principalId --role-definition-id $readOnlyRoleDefinitionId

We now can use this system managed identity from Azure Function App to access Cosmos DB with the permissions we defined for that role. To see all the assignments you made you execute this statement:

az cosmosdb sql role assignment list --account-name $accountName --resource-group $resourceGroupName

Access Azure CosmosDB with Azure Function App

Now start Visual Studio 2019 and create a new function app with a HTTP Trigger. Right click the new created project and select „Manage nuget packages…“. Make sure the checkbox „Include prerelease“ is checked! Add the following nuget packages to your solution:

  • Azure.Identity (Version 1.3.0)
  • Microsoft.Azure.Cosmos (Version 3.17.0-preview1)
Functionality is only in the preview of 3.17.0 but not in the final release!

Modify Function1.cs file as following. First replace the using statements with these:

using Azure.Identity;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;

Add a person class which represents our documents in Azure Cosmos DB

public class Person
{
    public string Id { get; set; }
    public string p { get; set; }
    public string First { get; set; }
}

Inside the class „public static class Function1“ write following two function. One to read an item and another to write an item. Make sure that you replace <yourAccount> with the name of your Azure Cosmos DB account name:

[FunctionName("GetItem")]
public static async Task<IActionResult> GetItem(
  [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
   ILogger log)
{
  try
    {
      // Since we know that we have a managed identity we instatiate that directly
      // var tokenCredential = new DefaultAzureCredential();
      ManagedIdentityCredential tokenCredential = new ManagedIdentityCredential();
      CosmosClient client = new
       CosmosClient("https://<yourAccount>.documents.azure.com:443/", tokenCredential);
      Container container = client.GetContainer("demodb", "democol");
      ItemResponse<Person> res = await container.ReadItemAsync<Person>("001", new PartitionKey("Test"));
      return new OkObjectResult(JsonConvert.SerializeObject(res.Resource));
    }
    catch (Exception ex)
    {
      return new BadRequestObjectResult(ex.ToString());
    }
}
[FunctionName("CreateItem")]
public static async Task<IActionResult> CreateItem(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
  try
  {
    ManagedIdentityCredential tokenCredential = new ManagedIdentityCredential();
    CosmosClient client = 
      new CosmosClient("https://<yourAccount>.documents.azure.com:443/", tokenCredential);
    Container container = client.GetContainer("demodb", "democol");
    ItemResponse<Person> res = await container.CreateItemAsync<Person>(
	new Person() 
	{ Id = "002", 
	  p = "Test", 
          First = "Sonja" }, 
	new PartitionKey("Test"));
    return new OkObjectResult(JsonConvert.SerializeObject(res.Resource));
  }
  catch (Exception ex)
  {
    return new BadRequestObjectResult(ex.ToString());
  }
}

Compile the application and publish it to the Azure function app (Right click project – Publish). Open the Azure portal again and navigate to your Azure function app. Under the section „Function“ select „Functions„. Select each function and aquirce a function URL with the „Get function Url“ buttom in the upper bar.

Azure Functions deployed

Now use a tool like Postman to try to execute your functions. While the GetItem-Function will return a result

GetItem Function returning data from Azure Cosmos DB

the CreateItem-Function will return an RBAC access error.

CreateItem Function returns an error claiming that the principal does not have the required RBAC permissions

Analyzing Loganalytics Azure CosmosDB Dataplane

Navigate now to your Azure Log Analytics Workspace in the Azure Portal. Under the section „General“ select „Logs„. Issuing the following statement will show the failed access to the document:

AzureDiagnostics 
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
  and Category == "DataPlaneRequests"
  and ResourceGroup == "COSMOSRBAC"
  and requestResourceType_s == "Document"
| where OperationName == 'Create'
| summarize by statusCode_s, OperationName, aadPrincipalId_g, aadAppliedRoleAssignmentId_g

Replacing the Operation Name with „Read“ on the other hand will show all 200 success code for that given principal id.

AzureDiagnostics 
| where ResourceProvider == "MICROSOFT.DOCUMENTDB"
  and Category == "DataPlaneRequests"
  and ResourceGroup == "COSMOSRBAC"
  and requestResourceType_s == "Document"
| where OperationName == 'Read'
| summarize by statusCode_s, OperationName, aadPrincipalId_g, aadAppliedRoleAssignmentId_g

I hope you enjoyed our tour!

Kind regards
AndiP

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

Blazor MS-Teams Tab App Series – Part 7

Welcome to the eigth and final post (0-7) of a series that will show you how you can write a Microsoft Teams Application with Blazor Webassembly.

You will find the complete source code in this github repository. I placed my changes in different branches so you can easily jump in at any time.

In this post we will secure our WeatherForeCast API in the Blazor-Server project and access it the same way we did with Microsoft Graph.

Webservice AAD App Registration

Click +New registration to register a new AAD application. Replace https://demo.ngrok.io with your own domain.

  • Name: BlazorService
  • Account Type: Accounts in any organizational directory (Mulitenant)
  • Redirect Uri: https://demo.ngrok.io

and click Register.

Make sure to copy your Application (Client) ID and store it for later. I am using the following Client-ID throughout the samples which need to be replaced with yours: 38a06d0f-e38d-4e93-be39-aa42d8ebe604

IMPORTANT

Microsoft Teams will only allow URLs that are in the premitted domain. Make sure that you use your NGROK or registered domain here!

Select Expose an API under the Manage section and set an application ID Uri. You can reuse the given UID from the proposed App ID Uri. For example:

  • api://spectologic.eu.ngrok.io/38a06d0f-e38d-4e93-be39-aa42d8ebe604

Now select +Add a scope to add the required "access_as_user" scope:

  • Scope Name : weather_api
  • Who can consent: Admins and Users
  • Provide some titel/text for the information that is displayed when the administrator of a tenant or a user is asked to grant consent.

Finally click Add scope to add the scope. You will get an url that looks like this:

  • api://spectologic.eu.ngrok.io/38a06d0f-e38d-4e93-be39-aa42d8ebe604/weather_api

To directly allow Teams to access we add the well known client IDs of the Microsoft Teams-Applications. There are two of them: Microsoft Teams Web Client and Microsoft Teams Desktop Client

Use the +Add a client application button to add the following client ids. You need to also select the scope "access_as_user" so that the Teams-Apps can access this specific scope!

  • Teams Web Application: 5e3ce6c0-2b1f-4285-8d4b-75ee78787346
  • Teams Mobile/Desktop App: 1fec8e78-bce4-4aaf-ab1b-5451cc387264

Adding the service to the BlazorApp AAD App Registration

Open the BlazorApp AAD application

Click on API permissions under the Manage section to add permissions to access our webservice. We do want to add delegated permissions (permissions in the context of the user). To add the following permission click + Add a permission and select My APIs / BlazorService and select delegated permissions:

  • weather_api

Afer clicking save make sure to click Grant admin consent for … button to grant these permissions within this tenant. So all users of this tenant do not have to explicitly consent to your MS-Teams App.

Securing the WeatherForeCast API

In BlazorTeamApp.Server project add following configuration to the appsettings.json file:

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "ClientId": "38a06d0f-e38d-4e93-be39-aa42d8ebe604",
  "TenantId": "common",
  "Audience": "api://demo.ngrok.io/38a06d0f-e38d-4e93-be39-aa42d8ebe604",
  "Issuer": "https://sts.windows.net/c409088f-0d18-498f-8ece-8659ea219c20/"
}

Add following nuget package to BlazorTeamApp.Server:

  • Microsoft.AspNetCore.Authentication.AzureAD.UI

In Startup.cs modify the method ConfigureService to add authentication:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = AzureADDefaults.AuthenticationScheme;
    }).AddJwtBearer("AzureAD", options =>
    {
        options.Audience = Configuration.GetValue<string>("AzureAd:Audience");
        options.Authority = Configuration.GetValue<string>("AzureAd:Instance")  
            + Configuration.GetValue<string>("AzureAd:TenantId");
        options.TokenValidationParameters = new 
            Microsoft.IdentityModel.Tokens.TokenValidationParameters()
            {
                ValidIssuer = Configuration.GetValue<string>(
                    "AzureAd:Issuer"),
                ValidAudience = Configuration.GetValue<string>(
                    "AzureAd:Audience")
            };
    });
 
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddTokenProvider();
}

Also in Startup.cs modify the method Configure(IApplicationBuilder,…:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ... 
    app.UseAuthentication();    // Add this line here
    // app.UseHttpsRedirection();
    app.UseBlazorFrameworkFiles();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthorization();    // Add this line here
    app.UseEndpoints(endpoints =>
    {
      ...

Now use the Authorize attribute to secure our WeatherForeCast controller by adapting Controllers/WeatherForeCastController.cs

...
namespace BlazorTeamApp.Server.Controllers
{
    [Authorize]     // Add this line
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
...

If you now start your application in Visual Studio and try to access this url:

you should receive a 401 error!

Access the Service from our Blazor Application

Since we have written all the necessary code before we can simply add a new button to our razor page and call our GetServerToken method to get an access token for our weather api:

Add this button to your Tab.razor page

<br  />
<button class="btn btn-light"  @onclick="GetWeatherServerAuthToken">Get Weather Server-Token</button>

Implement the button handler in Tab.razor

private string weatherAuthServerToken = string.Empty;
private async Task GetWeatherServerAuthToken()
{
    try
    {
        authServerToken = await TeamsClient.GetServerToken(authToken, 
           new string[1] { "api://spectologic.eu.ngrok.io/38a06d0f-e38d-4e93-be39-aa42d8ebe604/weather_api" });
    }
    catch (Exception ex)
    {
        authServerToken = $"Fehler: {ex.Message}";
    }
}

I leave implementing the call to the webservice with the gained token up to you. It is done the same way as we have called the Microsoft Graph me-Endpoint.

That’s it, have a great day

AndiP

Blazor MS-Teams Tab App Series – Part 6

Welcome to the seventh post (0-6) of a series that will show you how you can write a Microsoft Teams Application with Blazor Webassembly.

You will find the complete source code in this github repository. I placed my changes in different branches so you can easily jump in at any time.

In this post we will extend our library, so that we are able to create access tokens to access the Microsoft Graph.

Extending our typescript library

We create a new method that will take the ClientAuthToken and scopes of apis we want to access. We obtain the Tenant-ID from the MS-Teams context and send this to our backend (/auth/token) which will use application (client)id and client secret to create an access token:

 GetServerToken(clientSideToken: string, scopes: string[]): Promise<string> {
	try {
		const promise = new Promise<string>((resolve, reject) => {
			microsoftTeams.getContext((context) => {
				fetch('/auth/token', {
					method: 'post',
					headers: {
						'Content-Type': 'application/json'
					},
					body: JSON.stringify({
						'tid': context.tid,
						'token': clientSideToken,
						'scopes': scopes
					}),
					mode: 'cors',
					cache: 'default'
				})
					.then((response) => {
						if (response.ok) {
							return response.json();
						} else {
							reject(response.statusText);
						}
					})
					.then((responseJson) => {
						if (responseJson.error) {
							reject(responseJson.error);
						} else {
							const serverSideToken = responseJson;
							console.log(serverSideToken);
							resolve(serverSideToken);
						}
					});
			});

		});
		return promise;
	}
	catch (err) {
		alert(err.message);
	}
}   

Since we do want to call this method from our Blazor components we add additional methods to our JavaScript-Wrapper interface/class:

ITeamsClient

using System.Threading.Tasks;
 
namespace SpectoLogic.Blazor.MSTeams
{
    public interface ITeamsClient
    {
        ValueTask<string> GetClientToken();
        ValueTask<string> GetServerToken(string clientToken, string[] scopes);
        ValueTask<string> GetUPN();
    }
}

TeamsClient

using Microsoft.JSInterop;
using System.Threading.Tasks;
 
namespace SpectoLogic.Blazor.MSTeams
{
    public class TeamsClient : ITeamsClient
    {
        public TeamsClient(IJSRuntime jSRuntime)
        {
            _JSRuntime = jSRuntime;
        }
        private readonly IJSRuntime _JSRuntime;
 
        public ValueTask<string> GetUPN() => 
            _JSRuntime.InvokeAsync<string>(MethodNames.GET_UPN_METHOD);
        public ValueTask<string> GetClientToken() => 
            _JSRuntime.InvokeAsync<string>(MethodNames.GET_CLIENT_TOKEN_METHOD);
 
        public ValueTask<string> GetServerToken(
            string clientToken, string[] scopes) => _JSRuntime.InvokeAsync<string>(MethodNames.GET_SERVER_TOKEN_METHOD, clientToken, scopes);
 
        private class MethodNames
        {
            public const string GET_UPN_METHOD 
                = "BlazorExtensions.SpectoLogicMSTeams.GetUPN";
            public const string GET_CLIENT_TOKEN_METHOD 
                = "BlazorExtensions.SpectoLogicMSTeams.GetClientToken";
            public const string GET_SERVER_TOKEN_METHOD 
                = "BlazorExtensions.SpectoLogicMSTeams.GetServerToken";
        }
    }
}

Extend our Blazor Library

Add the following nuget packages to SpectoLogic.Blazor.MSTeams.csproj

  • Microsoft.Extensions.Http
  • System.Text.Json

Create a new subfolder auth within the project and add following classes:

TokenResponse.cs This class reassembles the token reference we get back from the OAuth endpoint.

using System.Text.Json.Serialization;
 
namespace SpectoLogic.Blazor.MSTeams.Auth
{
    public class TokenResponse
    {
        [JsonPropertyName("token_type")]
        public string TokenType { get; set; }
        [JsonPropertyName("scope")]
        public string Scope { get; set; }
        [JsonPropertyName("expires_in")]
        public int ExpiresIn { get; set; }
        [JsonPropertyName("ext_expires_in")]
        public int ExtExpiresIn { get; set; }
        [JsonPropertyName("access_token")]
        public string AccessToken { get; set; }
    }
}

TokenError.cs This class reassembles an error that we might get back from the OAuth endpoint. If for example we did not grant consent to our tenant and the user has not granted consent so far we will retrieve an error like ‚invalid_grant‘.

using System.Text.Json.Serialization;
 
namespace SpectoLogic.Blazor.MSTeams.Auth
{
    public class TokenError
    {
        /// <summary>
        /// May include things like "invalid_grant"
        /// </summary>
        [JsonPropertyName("error")]
        public string Error { get; set; }
        [JsonPropertyName("error_description")]
        public string Description { get; set; }
        [JsonPropertyName("error_codes")]
        public int[] Codes { get; set; }
        [JsonPropertyName("timestamp")]
        public string Timestamp { get; set; }
        [JsonPropertyName("trace_id")]
        public string TraceId { get; set; }
        [JsonPropertyName("correlation_id")]
        public string CorrelationId { get; set; }
        [JsonPropertyName("error_uri")]
        public string ErrorUri { get; set; }
    }
}

ITokenProvider.cs The interface for our server-side helper class that encapsulates the retrieval of the access token.

using System.Threading.Tasks;
 
namespace SpectoLogic.Blazor.MSTeams.Auth
{
    public interface ITokenProvider
    {
        Task<TokenResponse> GetToken(
            string tenantId, 
            string token, 
            string clientId, 
            string clientSecret, 
            string[] scopes);
    }
}

ITokenErrorException.cs An exception that will be thrown by the TokenProvider implementation, if we receive an error response from the OAuth Endpoint.

using System;
using System.Runtime.Serialization;
 
namespace SpectoLogic.Blazor.MSTeams.Auth
{
    public class TokenErrorException : Exception
    {
        public TokenError TokenError { get; set; }
 
        public TokenErrorException()
        {
        }
 
        public TokenErrorException(TokenError error) : base(error.Error)
        {
            TokenError = error;
        }
 
        public TokenErrorException(TokenError error, Exception innerException) : base(error.Error, innerException)
        {
            TokenError = error;
        }
 
        protected TokenErrorException(SerializationInfo info, StreamingContext context) : base(info, context)
        {
        }
    }
}

Tokenprovider.cs The implementation of our token provider accessing the Token-Endpoint. Future implementations should use the MSAL-Library from Microsoft, but for illustration purposes this should suffice:

using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
 
namespace SpectoLogic.Blazor.MSTeams.Auth
{
    public class TokenProvider : ITokenProvider
    {
        private readonly IHttpClientFactory _clientFactory;
 
        public TokenProvider(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }
  
        public async Task<TokenResponse> GetToken(
            string tenantId, 
            string token, 
            string clientId, 
            string clientSecret, 
            string[] scopes)
        {
            string requestUrlString = 
            $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
 
            Dictionary<string, string> values = new Dictionary<string, string>
            {
                { "client_id", clientId },
                { "client_secret", clientSecret },
                { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" },
                { "assertion", token },
                { "requested_token_use", "on_behalf_of" },
                { "scope", string.Join(" ",scopes) }
            };
            FormUrlEncodedContent content = new FormUrlEncodedContent(values);
 
            HttpClient client = _clientFactory.CreateClient();
            HttpResponseMessage resp = await client.PostAsync(
                requestUrlString, content);

            if (resp.IsSuccessStatusCode)
            {
                string respContent = await resp.Content.ReadAsStringAsync();
                return JsonSerializer.Deserialize<TokenResponse>(respContent);
            }
            else
            {
                string respContent = await resp.Content.ReadAsStringAsync();
               
                TokenError tokenError = 
                    JsonSerializer.Deserialize<TokenError>(respContent);

                throw new TokenErrorException(tokenError);
            }
        }
    }
}

TokenProviderExtensions And finally some class to implement the extension method which can be used by the server project to provide the classes via DI.

using Microsoft.Extensions.DependencyInjection;
 
namespace SpectoLogic.Blazor.MSTeams.Auth
{
    public static class TokenProviderExtensions
    {
        public static IServiceCollection AddTokenProvider(
            this IServiceCollection services)
            => services.AddScoped<ITokenProvider, TokenProvider>()
                .AddHttpClient();
    }
}

Use the library in our Blazor-Backend

Now let’s use this library in our Backend to implement the /auth/token service.

First of all add a reference of our razor library to the BlazorTeamTab.Server project.

Extend Startup.cs to call the extension method that will provider the necessary classes via DI:

...
using SpectoLogic.Blazor.MSTeams.Auth; // Add this line
...
services.AddRazorPages();
services.AddTokenProvider(); // Add this line
...

Add a folder Dtos and add the class TokenRequest to it. This reassembles the structure we receive from our typescript implementation:

using System.Text.Json.Serialization;
 
namespace SpectoLogic.Blazor.MSTeams.Auth
{
    public class TokenRequest
    {
        [JsonPropertyName("tid")]
        public string Tid { get; set; }
        [JsonPropertyName("token")]
        public string Token { get; set; }
        [JsonPropertyName("scopes")]
        public string[] Scopes { get; set; }
    }
}

Under Server/Controllers create a new file AuthController.cs We add ClientID/ClientSecret from our AAD-Application hardcoded! Do NOT do that in production code! Never store secrets in code! Use Azure KeyVault or other more secure locations.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SpectoLogic.Blazor.MSTeams.Auth;
using System;
using System.Threading.Tasks;
 
namespace BlazorTeamApp.Server.Controllers
{
    [ApiController]
    [Route("[controller]/[action]")]
    public class AuthController : ControllerBase
    {
        private readonly ILogger<AuthController> logger;
        private readonly ITokenProvider tokenProvider;
 
        public AuthController(
            ILogger<AuthController> logger, 
            ITokenProvider tokenProvider)
        {
            this.logger = logger;
            this.tokenProvider = tokenProvider;
        }
 
        [HttpPost]
        [ActionName("token")]
        public async Task<IActionResult> Token(
            [FromBody] TokenRequest tokenrequest)
        {
            string clientId = "e2088d72-79ce-4543-ab3c-0988dfecf2f1"; 
            // From Step 4
            string clientSecret = "_2xb_7h-P1voxOddv4jzaE8I_l-Tx71BlH"; 
            // From Step 4
            try
            {
                TokenResponse tokenResponse = await tokenProvider.GetToken(
                        tokenrequest.Tid,
                        tokenrequest.Token,
                        clientId,
                        clientSecret,
                        tokenrequest.Scopes);
                return new JsonResult(tokenResponse.AccessToken);
            }
            catch (TokenErrorException tokenErrorEx)
            {
                return new JsonResult(tokenErrorEx.TokenError);
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

Access Microsoft Graph from Blazor-Page

Since we do want to do HTTP-Requests lets add the HTTP-Client in Startup.cs

using SpectoLogic.Blazor.MSTeams;
...
builder.Services.AddMSTeams();
builder.Services.AddHttpClient(); // add this line!

await builder.Build().RunAsync();
...

Now implement Tab.razor page. We inject the HttpClientFactory and add two new methods to handle our two new buttons.

One will receive the server auth token for Microsoft Graph, where the other will use that token to fetch some basic user informations from the Graph.

@page "/tab"
@inject SpectoLogic.Blazor.MSTeams.ITeamsClient TeamsClient;
@inject IHttpClientFactory HttpFactory;
 
<h1>Counter</h1>
 
<p>Current count: @currentCount</p>
<p>Current User: @userName</p>
<p>AuthToken: @authToken</p>
<p>Server Token: @authServerToken</p>
<p>User Data: @userData</p>
 
<button class="btn btn-primary"  @onclick="IncrementCount">Click me</button>
<button class="btn btn-primary"  @onclick="GetUserName">Get UserName</button>
<br  />
<button class="btn btn-light"  @onclick="GetAuthToken">Get Auth-Token</button>
<button class="btn btn-light"  @onclick="GetServerAuthToken">Get Server-Token</button>
<button class="btn btn-light"  @onclick="GetUserData">Get User Data</button>
 
@code { 
 
    private int currentCount = 0;
    private string userName = string.Empty;
 
    private void IncrementCount()
    {
        currentCount++;
    }
 
    private async Task GetUserName()
    {
        userName = await TeamsClient.GetUPN();
    }
    private string authToken = string.Empty;
 
    private async Task GetAuthToken()
    {
        authToken = await TeamsClient.GetClientToken();
    }
 
    private string authServerToken = string.Empty;
 
    private async Task GetServerAuthToken()
    {
        try
        {
            authServerToken = await TeamsClient.GetServerToken(
                authToken, 
                new string[1] { "https://graph.microsoft.com/User.Read" });
        }
        catch (Exception ex)
        {
            authServerToken = $"Fehler: {ex.Message}";
        }
    }
 
    private string userData = string.Empty;
    private async Task GetUserData()
    {
        try
        {
            var client = HttpFactory.CreateClient();
            HttpRequestMessage request = new HttpRequestMessage()
            {
                Method = HttpMethod.Get,
                // https://graph.microsoft.com/v1.0/me/mailFolders('Inbox')/messages?$select=sender,subject&$top=2
                RequestUri = new Uri("https://graph.microsoft.com/v1.0/me/")
            };
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", authServerToken);
            authToken = $"Bearer {authServerToken}";
            var response = await client.SendAsync(request);
            userData = await response.Content.ReadAsStringAsync();
        }
        catch (Exception ex)
        {
            userData = $"Fehler: {ex.Message}";
        }
    }
}

In our final post for this series tomorrow, we will secure the Weatherforecast API and reuse what we have learned so far to access the secured service.

That’s it for today! See you tomorrow!

AndiP

Blazor MS-Teams Tab App Series – Part 5

Welcome to the sixth post (0-5) of a series that will show you how you can write a Microsoft Teams Application with Blazor Webassembly.

You will find the complete source code in this github repository. I placed my changes in different branches so you can easily jump in at any time.

In this post we will put our test code into a library, so that we can reuse our work in future projects. I oriented myself on an existing github project called BlazorExtensions/Storage to build this library.

Building a Razor Library base

Add a new project (of type: Razor Class Library) to your solution. I name it "SpectoLogic.Blazor.MSTeams":

  • .NET Core 3.1
  • No pages/Views

Add a folder Client in the project and add a typescript file named SpectoLogic.Blazor.MSTeams.ts (Feel free to adapt naming to what suits you best).

Add the nuget package Microsoft.TypeScript.MSBuild to the project.

Create a new Typescript JSON Configuration File and configure it like this, since we need promises and AMD (Asynchronous module definition).

{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": true,
    "module": "AMD",
    "target": "ES2016", 
    "outDir": "wwwroot"
  },
  "exclude": [
    "node_modules",
    "wwwroot"
  ]
}

If you compile your solution the file SpectoLogic.Blazor.MSTeams.js should be generated and placed in the wwwroot folder.

Unfortunatly you cannot add libman directly to your project. Therefore just copy libman.json from your BlazorTeamApp.Client project and insert it to your new library project.

Change libman.json as so:

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
 
    {
      "provider": "jsdelivr",
      "library": "@microsoft/teams-js@1.8.0",
      "destination": "Client/external/teams"
    }
  ]
}

Let’s implement Spectologic.Blazor.MSTeams.ts! This is basically all the javascript code we wrote before in index.html in a typescript class

import { Context } from "@microsoft/teams-js";
 
interface SpectoLogicTeamsInterface {
    Initialize(): void;
    GetUPN(): string;
    GetClientToken(): Promise<string>;
}
 
export class SpectoLogicTeams implements SpectoLogicTeamsInterface {
 
    private _context: Context;
 
    Initialize(): void {
        console.log("Initializing teams,...");
        try {
            microsoftTeams.initialize();
            microsoftTeams.getContext((context) => {
                this._context = context;
            });
        }
        catch (err) {
            alert(err.message);
        }
    }
 
    GetUPN(): string {
        return Object.keys(this._context).length > 0 ? this._context['upn'] : "";
    }
 
    GetClientToken(): Promise<string> {
        try {
                const promise = new Promise<string>((resolve, reject) => {
 
                    console.log("1. Get auth token from Microsoft Teams");
 
                    microsoftTeams.authentication.getAuthToken({
                        successCallback: (result) => {
                            resolve(result);
                        },
                        failureCallback: function (error) {
                            alert(error);
                            reject("Error getting token: " + error);
                        }
                    });
 
                });
            return promise;
        }
        catch (err) {
            alert(err.message);
        }
    }
}
 
export const blazorExtensions = 'BlazorExtensions';
// define what this extension adds to the window 
// object inside BlazorExtensions
export const extensionObject = {
    SpectoLogicMSTeams: new SpectoLogicTeams()
};
 
export function Initialize(): void {
    if (typeof window !== 'undefined' && !window[blazorExtensions]) {
        // when the library is loaded in a browser 
        // via a <script> element, make the
        // following APIs available in global 
        // scope for invocation from JS
        window[blazorExtensions] = {
            ...extensionObject
        };
    }
    else {
        window[blazorExtensions] = {
            ...window[blazorExtensions],
            ...extensionObject
        };
    }
    let extObj: any;
    extObj = window['BlazorExtensions'];
    extObj.SpectoLogicMSTeams.Initialize();
}

Adapting the BlazorTeamApp.Client project

Select the project properties of BlazorTeamApp.Client project and configure the TypeScript Build:

  • ECMA Script version: ECMAScript 2016
  • Module System: AMD

Then add a reference to SpectoLogic.Blazor.MSTeams project.

Add the requireJS library by adding this to the libman.js file:

{
  "provider": "cdnjs",
  "library": "require.js@2.3.6",
  "destination": "wwwroot/scripts/requirejs"
},

Replace the existing BlazorTeamApp.Client/wwwroot/index.html with this code which will initialize requireJS and call main.js:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorTeamApp</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
</head>

<body>
    <app>Loading...</app>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">
            Reload
        </a>
        <a class="dismiss">
            🗙
        </a>
    </div>
    <script src="_framework/blazor.webassembly.js">
    </script>
    <!-- Reference to the MS Teams Script added with libman -->

    <script src="scripts/teams/dist/MicrosoftTeams.js" />
    <script data-main="scripts/main" src="scripts/requirejs/require.js"/>
</body>
</html>

Create main.js under **BlazorTeamApp.Client/wwwroot/scripts/":

// Set up any config you need (you might not need this)
requirejs.config({
    basePath: "/scripts",
    paths: {
        "SpectoLogic.Teams": '/_content/SpectoLogic.Blazor.MSTeams/SpectoLogic.Blazor.MSTeams'
    }
}
);
 
// Tell RequireJS to load your main module (and its dependencies)
require(['require', 'SpectoLogic.Teams'], function (require) {
    var namedModule = require('SpectoLogic.Teams');
    namedModule.Initialize();
});

Implementing the rest of the library

Open SpectoLogic.Blazor.MSTeams.csproj file in an editor and configure the language version to 8.0. Also Add the nuget package Microsoft.JSInterop:

...
<PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <RazorLangVersion>3.0</RazorLangVersion>
    <LangVersion>8.0</LangVersion>
</PropertyGroup>
...
<PackageReference Include="Microsoft.JSInterop" Version="3.1.9" />
...

Now add a new file ITeamsClient.cs and definte it like so:

using System.Threading.Tasks;
 
namespace SpectoLogic.Blazor.MSTeams
{
    public interface ITeamsClient
    {
        ValueTask<string> GetClientToken();
        ValueTask<string> GetUPN();
    }
}

Implement the interface in a file TeamsClient.cs:

using Microsoft.JSInterop;
using System.Threading.Tasks;
 
namespace SpectoLogic.Blazor.MSTeams
{
    public class TeamsClient : ITeamsClient
    {
        public TeamsClient(IJSRuntime jSRuntime)
        {
            _JSRuntime = jSRuntime;
        }
        private readonly IJSRuntime _JSRuntime;
 
        public ValueTask<string> GetUPN() => 
          _JSRuntime.InvokeAsync<string>(MethodNames.GET_UPN_METHOD);
        public ValueTask<string> GetClientToken() => 
          _JSRuntime.InvokeAsync<string>(MethodNames.GET_CLIENT_TOKEN_METHOD);
 
        private class MethodNames
        {
            public const string GET_UPN_METHOD = 
                "BlazorExtensions.SpectoLogicMSTeams.GetUPN";
            public const string GET_CLIENT_TOKEN_METHOD = 
                "BlazorExtensions.SpectoLogicMSTeams.GetClientToken";
        }
    }
}

Add an extension class to allow easy injection into the DI by implementing MSTeamsExtensions.cs:

using Microsoft.Extensions.DependencyInjection;
 
namespace SpectoLogic.Blazor.MSTeams
{
    public static class MSTeamsExtensions
    {
        public static IServiceCollection AddMSTeams   
          (this IServiceCollection services) 
            => services.AddScoped<ITeamsClient, TeamsClient>();
    }
}

Use library in Blazor-Teams App

Add our new component to BlazorTeamApp.Client/Program.cs by using our extension method:

public class Program
{
  public static async Task Main(string[] args)
  { 
    ...
    builder.Services.AddScoped(sp => new HttpClient 
      { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    builder.Services.AddMSTeams();
    ...
  }
}

and rewrite the Tab.razor page

@page "/tab"
@inject SpectoLogic.Blazor.MSTeams.ITeamsClient TeamsClient;
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<p>Current User: @userName</p>
<p>AuthToken: @authToken</p>
<button class ="btn btn-primary"  @onclick="IncrementCount">Click me</button>
<button class ="btn btn-primary"  @onclick="GetUserName">Get UserName</button>
<br />
<button class="btn btn-light" @onclick="GetAuthToken">Get Auth-Token</button>
@code {
    private int currentCount = 0;
    private string userName = string.Empty;
    private void IncrementCount()
    {
        currentCount++;
    }
    private async Task GetUserName()
    {
        userName = await TeamsClient.GetUPN();
    }
    private string authToken = string.Empty;
    private async Task GetAuthToken()
    {
        authToken = await TeamsClient.GetClientToken();
    }
}

In our next post we will extend this library to finally access MS-Graph!

That’s it for today! See you tomorrow!

AndiP