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

Blazor MS-Teams Tab App Series – Part 4

Welcome to the fifth post (0-4) 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.

Fetching access tokens,…

To create some access tokens we first need to create an Azure Active Directory Application Registration for our Blazor-Tab Application.

Therefore navigate to https://portal.azure.com and select your tenant. Then switch to Azure Active Directory and select App registrations from the left panel. You can navigate there directly by using this link.

AAD App Registration

Click +New registration to register a new AAD application

  • Name: BlazorApp
  • Account Type: Accounts in any organizational directory (Mulitenant)
  • Redirect Uri: Leave that empty for now

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: a97275e3-965d-4076-90dd-76cce240e8fb

IMPORTANT

Microsoft Teams will only allow AAD Application Scopes/Redirect 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://demo.ngrok.io/a97275e3-965d-4076-90dd-76cce240e8fb

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

  • Scope Name : access_as_user
  • 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://demo.ngrok.io/a97275e3-965d-4076-90dd-76cce240e8fb/access_as_user

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

Since we also want to create further access tokens for Microsoft Graph and our own API with this AAD-Application we create a client secret that we will use to create those tokens. Make sure that the client secret is never stored in a client application! It should always be in a trusted environment.

Click on Certificates & Secrets under the Manage section and create a new client secret by selecting +New client secret

Also copy that client secret. I am using this one (it is invalid now, of course):

  • B._12b.spZ391IGzr-Yg0T09Ug9D1erP.f

Click on API permissions under the Manage section to add permissions to access the graph. We do want to add delegated permissions (permissions in the context of the user). Add the following permissions, if not already defined:

  • User.Read
  • email
  • offline_access
  • OpenId
  • profile

In case you are interessted in knowing more about the offline_access permission, you can read this article.

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.

Finally select Authentication under the Manage section to add a redirect url for access tokens. We will add the server part later!

  • Click +Add a platform and select Web
  • Add a redirect Url to your application: https://demo.ngrok.io/auth-end
  • Scroll down to Implicit grant section and make sure that we can obtain the tokens from the authorization endpoint. Select both options:
    • ID-Token (Checked)
    • Access-Token (Checked)

Update your Teams Application Manifest and redeploy

After we have registered our AAD Application we want to configure our MS-Teams App to use it.

Open your Visual Studio project and click CTRL+Q to get to the general search. Type "teams" to get the option to view the Microsoft Teams Tool Kit.

As before click edit app-package and select development (some uid). Under Domains and permissions set:

  • AAD App ID: previously created app id: a97275e3-965d-4076-90dd-76cce240e8fb
  • Single Sign On: previously created app uri: api://demo.ngrok.io/a97275e3-965d-4076-90dd-76cce240e8fb

Click Update to update the application package.

This will add this section to the manifest:

...
  ],
  "validDomains": [],
  "webApplicationInfo": {
	    "id": "a97275e3-965d-4076-90dd-76cce240e8fb",
	    "resource": "api://demo.ngrok.io/a97275e3-965d-4076-90dd-76cce240e8fb"
  }
}

Make sure to use your own AppID/URIs and remove your app from teams and redeploy the app with the new development.zip file!

Getting the Auth-Token

Now let’s write some code to get the Auth-Token that we can later use to get access tokens for MS-Graph and our own API.

First add this javascript code to the index.html page. Again we use the MS-Teams SDK to get the auth token.

function getClientSideToken() {
  try {
          return new Promise((resolve, reject) => {
          console.log("Get auth token from Microsoft Teams");
          microsoftTeams.authentication.getAuthToken({
              successCallback: (result) => {
                  resolve(result);
              },
              failureCallback: function (error) {
                  alert(error);
                  reject("Error getting token: " + error);
              }
          });
      });
  }
  catch (err) {
      alert(err.message);
  }
}

From Blazor we can call this function as we did before by adding a new button and a call to the Javascript Runtime:

...
<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 async Task GetUserName()
{
    userName = await JSRuntime.InvokeAsync<string>("GetUPN");
}
private string authToken = string.Empty;
 
private async Task GetAuthToken()
{
    authToken = await JSRuntime.InvokeAsync<string>("getClientSideToken");
}
...

Rebuild your application and run it in Visual Studio Debugger. Make sure you have updated your Teams app, then start teams and reload your personal tab. Initialize the Teams-SDK and then try to get the auth token.

Copy the authtoken and paste it to a https://jwt.ms or https://jwt.io to examine the auth token.

That’s it for today! See you tomorrow!

AndiP

Blazor MS-Teams Tab App Series – Part 3

Welcome to the forth post 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.

Accessing Teams SDK from Blazor

Before we try to abstract the JavaScript away from our Blazor Application we take a climpse to the JavaScript-World and how we interact from there with our Blazor application.

Before you can access the MS-Teams Context you need to initialize the SDK, which can be simple done by calling the initialize method! The Context contains various key/values where one is the user principal name (upn).

I put these two functionalities in two javascript functions and added this to my index.html page.

Add this code to BlazorTeamApp.Client/wwwroot/index.html:

<script>
var _context;

function InitializeTeamsContext() {
    console.log("InitializeTeamsContext called...");
    try {
      // Initialize SDK
      microsoftTeams.initialize();
      // Fetch MS-Teams Context and store it in the _context variable
      microsoftTeams.getContext((context, error) => {
      _context = context;
    });
  }
  catch (err) {
    alert(err.message);
  }
}

function GetUPN() {
  console.log("GetUPNCalled");
  // Get User Principal Name
  let userName = Object.keys(_context).length > 0 ? _context['upn'] : "";
  return userName;
}
</script>

So how do we call these JavaScript functions from Blazor? By using the JSRuntime class. We inject a instance to our Blazor Page using the @inject command. In the future we want to initialize MS-Teams when the page loads. Right now we require the user to click the buttons in the right order:

Edit BlazorTeamApp.Client/Pages/Tab.razor

@page "/tab"
@inject IJSRuntime JSRuntime;
 
<h1>Counter</h1>
 
<p>Current count: @currentCount</p>
<p>Current User: @userName</p>
 
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<button class="btn btn-primary" @onclick="InitTeams">Init Teams</button>
<button class="btn btn-primary" @onclick="GetUserName">Get UserName</button>
 
@code {
    private int currentCount = 0;
    private string userName = string.Empty;
 
    private void IncrementCount()
    {
        currentCount++;
    }
 
    private async Task InitTeams()
    {
        await JSRuntime.InvokeVoidAsync("InitializeTeamsContext");
    }
    private async Task GetUserName()
    {
        userName = await JSRuntime.InvokeAsync<string>("GetUPN");
    }
}

Hit F5 in Visual Studio to start the application again and switch back to MS-Teams. Hit the refresh button to reload you Team-Tab.

Then click the Init Teams button to initialize the Teams SDK and then Get UserName to display the username of the current logged in user in teams.

Voila! Your first information fetched from MS-Teams. In the next blog post I show you how to get hands on the client auth token that we will use later to get access tokens for Graph and our own services.

That’s it for today! See you tomorrow!

AndiP

Blazor MS-Teams Tab App Series – Part 2

Welcome to the third post (developers count from 0) 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.

Configure the Teams Deployment package

Today we’ll have a look at the teams application deployment package and manifest. Each MS-Teams project contains two deployment packages. One for development and one for production. You can configure some basic settings with the corresponding development.json and production.json files:

There are more files in that folder as we can see if we open explorer:

The Microsoft Teams Toolkit extension will generate the zip-files which are the deployment packages. If you open a zip file you will see that it contains a manifest.json file.

Instead of editing these files manually we use the integrated Manifest Editor that come with the Teams Toolkit extension. It can be easily found by opening the general search in Visual Studio CTRL+Q and typing teams:

Edit app package by selecting the menu "Edit app package":

Then select development! It is necessary that you are logged in to your tenant (Use Welcome Menue and the Button in the upper right). If you do this the first time, the toolkit will generate a unique ID for your teams application.

Unfortunatly when you move your project somewhere else and other constellations i have not figured out yet it can happen that it cannot find your deployment package and you end up with this weird error message:

What now, you might ask? Well, I solved it by removing the GUID from the development.json file. No worries you won’t loose anything aside of your UID:

{
  "baseUrl0": "https://localhost:44310",
  "version": "1.0.0",
  "appname": "BlazorTab",
  "fullappname": "BlazorTab",
  "teamsAppId": "3bffcaf5-3c25-48a6-9128-7be501f5516e" // <--- Remove this value
}
{
  "baseUrl0": "https://localhost:44310",
  "version": "1.0.0",
  "appname": "BlazorTab",
  "fullappname": "BlazorTab",
  "teamsAppId": ""
}

Then try to edit the manifest again. MS Teams Toolkit will now generate a new UID and you should be good to go again. Sometimes the right editor window does not render correctly. Selecting the left topics "App details" & "Tabs" back and forth will fix this.

App details is quite self explanatory and I won’t walk you through that in detail. Just make sure that the URLs are adapted to your ngrok domain.

For example:

Under Capabilities / Tab we configure our TAB application. If you have a TAB that can be added to a team you need to provide a configuration URL where your end users will customize you Teams App for that Team-Channel. Configure a URL there. For example:

By clicking on the … right to the configuration url you can also define the scope of your Teams Tab application and if the configuration is readonly.

Add a many personal tabs as you like. Configure them accordingly. Each tab should have a name and content url. For example:

Microsoft Teams will replace some variables in the content URL. In the example above it will replace {entityid} with "index". You can find a list of supported context keys here.

You could f.e. create multiple tabs with the same content URL but using different EntityIDs.

Deploy to teams (Sideload)

  1. Open the Microsoft Teams Client
  2. In the application bar select Apps and then Upload a custom app (left at the bottom)

Select the file development.zip from your .publish folder, install the application and pin it to your left Teams Taskbar.

Make sure ngrok and your Teams App is running in Visual Studio before you click on you application icon. If you have done everything right, you should now see your /tab razor page within Microsoft Teams.

That’s it for today! See you tomorrow!

AndiP

Blazor MS-Teams Tab App Series – Part 1

Welcome to the second post 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.

Today we continue with our base MS-Teams project. Since our service must run on a public available domain I recommend using NGROK (yesterday I learned it is spoken like this: [n g rock] ) which is a tunneling service that offers a public URL that will tunnel a request to your local machine.

An additional benefit is that NGROK also provides a SSL-Endpoint terminating at the tunneling site, so you can easily server HTTPS without worring about certificates.

Warning: If you use the free edition of NGROK your domain name will change every hour or so. Since we will also configure Azure Active Directory Applications in AAD this can be tedious to constantly adapt. I would recommend to buy the base version or to host the application on your domain.

Let’s start! Add a new "Blazor"-Project in Visual Studio to our existing solution:

a. Project-Name: BlazorTeamApp
b. Select "Blazor WebAssembly App"
c. Configure for HTTPS: YES
d. ASP.NET Core hosted: YES

Tab-Route

Next we reuse the Counter.razor page as our first tab page. To name it properly we rename Counter.razor to Tab.razor. Make sure that all your razor pages follow the Pascal naming convention otherwise Visual Studio will complain!

In the file BlazorTeamApp.Client/Pages/Tab.razor replace

  • @page "/counter" with @page "/tab"

Remove Sidebar

Since Teams has it’s own navigation we remove the sidebar by removing following code in the file BlazorTeamApp.Client/Shared/MainLayout.razor:

<div class="sidebar">
    <NavMenu />
</div>

Add the MS-Teams SDK Javascript Library

Right click the project "BlazorTeamApp.Client" and select "Manage Client Side Libraries". Then replace the content of the file libman.json with the following code.

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

The folder BlazorTeamApp.Client/wwwroot/scripts/teams should have been created!

Lets add the MicrosoftTeams.js file to the index.html page to have it run at startup!

Edit BlazorTeamApp.Client/wwwroot/index.html

...
    
    <!--Reference to the MS Teams Script added with libman -->
    

...

Do not use IIS Express

IIS Express does not allow your site beeing called from outside. So NGROK can not tunnel to a IIS Express hosted site running on an address like "https://localhost:44383/tab&quot;.

Open the project settings of BlazorTeamApp.Server and select the settings for Debug:

  1. Change the profile to "BlazorTeamApp.Server"
  2. Set Launch Browser to: http://localhost:5000

Since we use the NGROK Https Endpoint we remove the HttpsRedirection in Startup.cs:

Comment out the HttpsRedirection in "BlazorTeamAp.Server/Startup.cs":

...
// app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
...

Once you have setup NGROK with your account open a terminal window and tunnel port 5000 to your NGROK sub domain. Adapt the region depending on your setup:

	ngrok http -region=eu -hostname=".eu.ngrok.io" 5000

Start the "BlazorTeamApp.Server" (not IIS Express!) in the debugger and validate that you can browse the following urls (replace demo.eu.ngrok.io with your ngrok domain):

Test the TAB and Web API:

That’s it for today! See you tomorrow!

AndiP

Blazor MS-Teams Tab App Series – Part 0

This is the first post of a series that will show you how you can write a Microsoft Teams Application with Blazor Webassembly. This post will guide you to get everything setup to be able to follow along the series.

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.

First I recommend setting up you own O365 tenant for development, as you might not have administrative priviledges on your production tenant.

  1. Setup your own Office 365 – Development Environment

  2. Navigate to Teams Admin Center, which also can be found in the M365 Admin Center under More/Admin Centers/Teams

    • Under Teams Apps / Manage Apps click on the upper right button Org-wide app settings and make sure to activate "allow interaction with custom apps
    • Unter Teams Apps / Setup policies / Global (Org-wide default) activate the option Upload custom apps and save! This can take up to 24 hours
  3. Enable the Developer Preview in Microsoft Teams Desktop Client

    All the settings in step 1 are required to be completed to be able to see those options!

    • Start TeamsApp and select your profile picture / About / Developer Preview and accept. Then log in again!
  4. Enable Developer Tools in Teams App (requires Developer Preview)

    • Close TeamsApp (Quit in Taskbar!)
    • Restart TeamsApp and Log-In again
    • Open a TAB-Application
    • Right-Click the Taskbar-Icon of Microsoft Teams App / Open Dev Tools (Context Menu)
  5. Install App Studio Teams App

    This teams application can help you craft your application manifest.

    • In Microsoft Teams App click on Apps Icon (Taskbar) then search for App Studio and install.
  6. Take the blue/red pill aka choose between .NET and React

    A MS Teams TAB application is basically a website running within an iframe in Teams. So basically you can write your application in any language and framework. To interact with teams you need to work with the JavaScript API. Microsoft offers extensions for Visual Studio Code and Visual Studio that can ease the setup of your projects. As of today Visual Studio Code creates a React-Template while Visual Studio will create an ASP.NET Core 3.1 application.

    • Visual Studio Code (only React-Template with NodeJS)

      • Add Extension Microsoft Teams Toolkit
    • Visual Studio (.NET Core 3.1)

      • Add Extension Microsoft Teams App After installing the extension you can add a new Microsoft Teams App.

If you want to follow my Step-by-Step Teams-App series please create a new Microsoft Teams App BlazorTab.

    Options:
        -  .NET Framework 4.8 (No worries it creats a .NET Core 3.1 App)
	Capabilities
		- Select TAB
		- Select Both 

    Visual Studio will create a new solution with two folders:
    - .publish
    - Tabs (.NET Core App 3.1)

That’s it for today! See you tomorrow!

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