Embedding Power BI Reports using Service Principal with Certificate with Microsoft .NET and React.
About PowerBI
Power BI Embedded is an Azure service that enables developers to add visualizations and reports to web or mobile applications, Power BI Embedded is intended for applications that are provided for third-party use. Power BI Embedded uses an application token authentication model to control access to the Power BI workspaces used by the application
Scenario
In today’s world of modern web development, For any Organization secure handling of authentication tokens and sensitive data is critical. Here we will be talking more about PowerBI embedding into UI using ServicePrincipal with certificate rather than using Secrets. We will see how we can embed PowerBI into React with Microsoft .NET Core (as backend) API with the help of generating JWT (tokens) and embedding URLs for Power BI reports.
Problem Statement:
One of the main reason for writing this article is because most of the articles over Internet which talks about Embedding PowerBI using Service Principal with Secret however not many which describes the way to use Certificate.
Another Intention is mainly for Management overhead of SPN’s if your Project is using many SPN’s with different authentication methods such as with Certificates as well as Secrets it is difficult to control and maintain them as requester/members stays with the team(s) or leaves the team(s) based on the requirement.
Introduction
The goal is to create an ASP.NET Core Web API that securely generates JWTs and access tokens required to interact with the Power BI API. Here we will cover different steps to achieve this:
- Requesting new SPN with Certificate.
- Create new Certificate and Upload to Azure Keyvault.
- Uploading certificate to App Registration.
- Assigning Valid Client API Permissions.
- Generate JWTs using these secrets.
- Use the JWTs to obtain embed URLs and generate access tokens for Power BI reports.
Solution
- First step is to request for a new Service Principal with certificate, for this login to your Organization Microsoft Entra ID. Click on App registration → New registration → Assign some name → if you want to assign some redirect URL then please add a name select the method and click register.
- Now lets request for creation of new Certificate and then Upload onto Azure Keyvault. Here I am uploading newly created .pfx file to Azure Keyvault.
Next make sure to add .pfx file secret under Secrets in Azure Keyvault.
3. Once the registration is done then click on “Certificates and Secrets” option → upload your newly created certificate. you can find more information about how to create SPN with certificate at “https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#option-1-upload-a-certificate
Here you can see I just uploaded a new certificate.
4. Next we need to assign permissions to newly create App registration. So click on API permissions → Click on + Add a permission → Under Microsoft API’s select Power BI service →click on Delegated permissions → Now lets add below permissions which are essential for any Power BI Embedding.
Next make sure you add SPN Client/AppID to your PowerBI permissions on your workspace.
5. Now the next step is to Integration Azure Keyvault with Microsoft .Net core application and retrieve SPN to generate a JWT token for us and Interact with PowerBI. Lets check further steps in Microsoft .net
Generating Token using Microsoft .Net with React.
- First step is to create a new Microsoft .Net Project, make sure to add relevant Packages to pull x509 certificate from Azure Keyvault. Here I have specifically used
- Azure.Identity
- Azure.Security.Keyvault.Secrets
- Microsoft.Identity.Client
- Microsoft.IdentityModel.Tokens
- System.IdentityModel.Tokens.Jwt
2. Next Look into the code so the first step is to defining JwtController class which will be responsible for generating JWT token and embed PowerBI URL.
namespace WebAPI
{
[Route("api/[controller]")]
public class JwtController : ControllerBase
{
private readonly ILogger<JwtController> _logger;
public JwtController(ILogger<JwtController> logger)
{
_logger = logger;
}
3. Next we need to add a method to generate JWT token, for this I am using GET method to retrieve secret from Azure Keyvault.
[HttpGet]
public async Task<IActionResult> GetJwt()
{
try
{
var keyVaultUrl = "https://keyvaultname.vault.azure.net/";
var secretClient = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
string clientId = secretClient.GetSecret("spn-appid").Value.Value;
string tenantId = secretClient.GetSecret("spn-tenantid").Value.Value;
// Retrieve the certificate from Key Vault
var certificateSecret = secretClient.GetSecret("spn-secret");
var certificateBytes = Convert.FromBase64String(certificateSecret.Value.Value);
var cert = new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.MachineKeySet);
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithTenantId(tenantId)
.WithCertificate(cert)
.Build();
var result = await app.AcquireTokenForClient(new string[] { "https://analysis.windows.net/powerbi/api/.default" })
.ExecuteAsync();
return Ok(result.AccessToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating JWT");
return StatusCode(500, "Error generating JWT");
}
}
One of the Important item to remember PowerBI Always works Analysis scope in Azure. So make sure you use “https://analysis.windows.net/powerbi/api/.default”
4. Next we need to Retrieve PowerBI embed URL method which uses the JWT generated by GetJwt
to authenticate and call the Power BI API, retrieving the embed URL for a specific report.
Your PowerBI API response URL should be a combination of GroupID and ReportID, So make sure you replace them accordingly.
https://api.powerbi.com/v1.0/myorg/groups/xxxxxxxxxxxxxxx/reports/xxxxxxxxxxxxxxx
[HttpGet("embedUrl")]
public async Task<IActionResult> GetEmbedUrl()
{
try
{
var jwtResult = await GetJwt();
if (jwtResult is OkObjectResult okResult)
{
var jwt = okResult.Value as string;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var powerBiResponse = await httpClient.GetAsync("https://api.powerbi.com/v1.0/myorg/groups/xxxxxxxxxxxxxxx/reports/xxxxxxxxxxxxxxx");
powerBiResponse.EnsureSuccessStatusCode();
var powerBiResponseContent = await powerBiResponse.Content.ReadAsStringAsync();
var powerBiData = JsonConvert.DeserializeObject<dynamic>(powerBiResponseContent);
if (powerBiData == null)
{
_logger.LogError("Power BI API response is null or empty");
return StatusCode(500, "Error getting embed URL");
}
string embedUrl = powerBiData.embedUrl;
return Ok(embedUrl);
}
else
{
return StatusCode(500, "Error generating JWT");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting embed URL");
return StatusCode(500, "Error getting embed URL");
}
}
5. Next step is to Generate Access Tokens for Embedding, here I am using Post request for the reports which I want to embedd. Make sure to add relevant Dataset and Report ID.
[HttpPost("generateToken")]
public async Task<IActionResult> GenerateToken()
{
try
{
var jwtResult = await GetJwt();
if (jwtResult is OkObjectResult okResult)
{
var jwt = okResult.Value as string;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var requestBody = new
{
datasets = new[] { new { id = "xxxxxxxxxxxxxxx" } },
reports = new[] { new { id = "xxxxxxxxxxxxxxx" } }
};
var requestBodyJson = JsonConvert.SerializeObject(requestBody);
var httpContent = new StringContent(requestBodyJson, Encoding.UTF8, "application/json");
var powerBiResponse = await httpClient.PostAsync("https://api.powerbi.com/v1.0/myorg/GenerateToken", httpContent);
powerBiResponse.EnsureSuccessStatusCode();
var powerBiResponseContent = await powerBiResponse.Content.ReadAsStringAsync();
_logger.LogInformation($"Power BI API response content: {powerBiResponseContent}");
var powerBiData = JsonConvert.DeserializeObject<dynamic>(powerBiResponseContent);
if (powerBiData == null)
{
_logger.LogError("Power BI API response is null or empty");
return StatusCode(500, "Error getting embed URL");
}
string accessToken = powerBiData.token;
if (string.IsNullOrEmpty(accessToken))
{
_logger.LogError("Access token is null or empty");
return StatusCode(500, "Error getting access token");
}
return Ok(accessToken);
}
else
{
return StatusCode(500, "Error generating JWT");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating access token");
return StatusCode(500, "Error generating access token");
}
}
}
}
6. Now for front end React code we have to use generated token from Microsoft .net and use it in React. For this I am using below code
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { PowerBIEmbed } from 'powerbi-client-react';
import { models } from 'powerbi-client';
import './App.css';
function App() {
const [embedConfig, setEmbedConfig] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const fetchEmbedConfig = async () => {
const tokenResponse = await axios.post('http://localhost:5035/api/jwt/generateToken');
const embedUrlResponse = await axios.get('http://localhost:5035/api/jwt/embedUrl');
setEmbedConfig({
type: 'report',
id: 'xxxxxxxxxxxx', // Replace with your actual Report ID
embedUrl: embedUrlResponse.data,
accessToken: tokenResponse.data,
tokenType: models.TokenType.Embed,
settings: {
panes: {
filters: {
expanded: false,
visible: true
}
},
}
});
};
fetchEmbedConfig();
// Update the current time every second
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
// Clear the interval when the component is unmounted
return () => clearInterval(timer);
}, []);
return (
<section className="App">
<h1>Power BI Testing(BETA Version) - {currentTime.toLocaleTimeString()}</h1>
<section id="bi-report" className="fullscreen">
{embedConfig &&
<PowerBIEmbed
embedConfig={embedConfig}
eventHandlers={
new Map([
['loaded', function () { console.log('Report loaded'); }],
['rendered', function () { console.log('Report rendered'); }],
['error', function (event) { console.log(event.detail); }],
['visualClicked', () => console.log('visual clicked')],
['pageChanged', (event) => console.log(event)],
])
}
cssClassName={"fullscreen"}
getEmbeddedComponent={(embeddedReport) => {
window.report = embeddedReport; // save report in window object
}}
/>
}
</section>
</section>
);
}
export default App;
7. Final step is to generate a token and test it, So I am running the project in Visual studio
This is how the dashboard looks
8. Also to test generated token via Postman
Also You can try many other API response calls such as getting capacity details etc.
Conclusion
This article describes how to securely generate JWTs and access tokens using Azure Key Vault and integrate with the Power BI API in an ASP.NET Core application. By following these steps, you can ensure secure handling of sensitive information and seamless interaction with Power BI for embedding reports and generating tokens. This approach not only enhances security but also simplifies the management of secrets and tokens in your applications.
Full Code:
React:
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { PowerBIEmbed } from 'powerbi-client-react';
import { models } from 'powerbi-client';
import './App.css';
function App() {
const [embedConfig, setEmbedConfig] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const fetchEmbedConfig = async () => {
const tokenResponse = await axios.post('http://localhost:5035/api/jwt/generateToken');
const embedUrlResponse = await axios.get('http://localhost:5035/api/jwt/embedUrl');
setEmbedConfig({
type: 'report',
id: 'xxxxxxxxxxxx', // Replace with your actual Report ID
embedUrl: embedUrlResponse.data,
accessToken: tokenResponse.data,
tokenType: models.TokenType.Embed,
settings: {
panes: {
filters: {
expanded: false,
visible: true
}
},
}
});
};
fetchEmbedConfig();
// Update the current time every second
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
// Clear the interval when the component is unmounted
return () => clearInterval(timer);
}, []);
return (
<section className="App">
<h1>Power BI Testing(BETA Version) - {currentTime.toLocaleTimeString()}</h1>
<section id="bi-report" className="fullscreen">
{embedConfig &&
<PowerBIEmbed
embedConfig={embedConfig}
eventHandlers={
new Map([
['loaded', function () { console.log('Report loaded'); }],
['rendered', function () { console.log('Report rendered'); }],
['error', function (event) { console.log(event.detail); }],
['visualClicked', () => console.log('visual clicked')],
['pageChanged', (event) => console.log(event)],
])
}
cssClassName={"fullscreen"}
getEmbeddedComponent={(embeddedReport) => {
window.report = embeddedReport; // save report in window object
}}
/>
}
</section>
</section>
);
}
export default App;
Microsoft .NET as backend API
using System.Security.Cryptography.X509Certificates;
using Azure.Security.KeyVault.Secrets;
using Azure.Identity;
using Microsoft.Identity.Client;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http.Headers;
using Newtonsoft.Json;
using System.Text;
namespace WebAPI
{
[Route("api/[controller]")]
public class JwtController : ControllerBase
{
private readonly ILogger<JwtController> _logger;
public JwtController(ILogger<JwtController> logger)
{
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> GetJwt()
{
try
{
var keyVaultUrl = "https://keyvaultname.vault.azure.net/";
var secretClient = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
string clientId = secretClient.GetSecret("spn-appid").Value.Value;
string tenantId = secretClient.GetSecret("spn-tenantid").Value.Value;
// Retrieve the certificate from Key Vault
var certificateSecret = secretClient.GetSecret("spn-certificate-Name"); //make sure to add reference name from Azure Keyvault certificate section
var certificateBytes = Convert.FromBase64String(certificateSecret.Value.Value);
var cert = new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.MachineKeySet);
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithTenantId(tenantId)
.WithCertificate(cert)
.Build();
var result = await app.AcquireTokenForClient(new string[] { "https://analysis.windows.net/powerbi/api/.default" })
.ExecuteAsync();
return Ok(result.AccessToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating JWT");
return StatusCode(500, "Error generating JWT");
}
}
[HttpGet("embedUrl")]
public async Task<IActionResult> GetEmbedUrl()
{
try
{
var jwtResult = await GetJwt();
if (jwtResult is OkObjectResult okResult)
{
var jwt = okResult.Value as string;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var powerBiResponse = await httpClient.GetAsync("https://api.powerbi.com/v1.0/myorg/groups/xxxxxxxxxxxxxxx/reports/xxxxxxxxxxxxxxx");
powerBiResponse.EnsureSuccessStatusCode();
var powerBiResponseContent = await powerBiResponse.Content.ReadAsStringAsync();
var powerBiData = JsonConvert.DeserializeObject<dynamic>(powerBiResponseContent);
if (powerBiData == null)
{
_logger.LogError("Power BI API response is null or empty");
return StatusCode(500, "Error getting embed URL");
}
string embedUrl = powerBiData.embedUrl;
return Ok(embedUrl);
}
else
{
return StatusCode(500, "Error generating JWT");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting embed URL");
return StatusCode(500, "Error getting embed URL");
}
}
[HttpPost("generateToken")]
public async Task<IActionResult> GenerateToken()
{
try
{
var jwtResult = await GetJwt();
if (jwtResult is OkObjectResult okResult)
{
var jwt = okResult.Value as string;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var requestBody = new
{
datasets = new[] { new { id = "xxxxxxxxxxxxxxx" } },
reports = new[] { new { id = "xxxxxxxxxxxxxxx" } }
};
var requestBodyJson = JsonConvert.SerializeObject(requestBody);
var httpContent = new StringContent(requestBodyJson, Encoding.UTF8, "application/json");
var powerBiResponse = await httpClient.PostAsync("https://api.powerbi.com/v1.0/myorg/GenerateToken", httpContent);
powerBiResponse.EnsureSuccessStatusCode();
var powerBiResponseContent = await powerBiResponse.Content.ReadAsStringAsync();
_logger.LogInformation($"Power BI API response content: {powerBiResponseContent}");
var powerBiData = JsonConvert.DeserializeObject<dynamic>(powerBiResponseContent);
if (powerBiData == null)
{
_logger.LogError("Power BI API response is null or empty");
return StatusCode(500, "Error getting embed URL");
}
string accessToken = powerBiData.token;
if (string.IsNullOrEmpty(accessToken))
{
_logger.LogError("Access token is null or empty");
return StatusCode(500, "Error getting access token");
}
return Ok(accessToken);
}
else
{
return StatusCode(500, "Error generating JWT");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating access token");
return StatusCode(500, "Error generating access token");
}
}
}
}
Also Console app code if you are starting your PowerBI embedding Journey.
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Identity.Client;
class Program
{
static void Main(string[] args)
{
var keyVaultUrl = "https://wellognonprodkeyvault.vault.azure.net/";
var secretClient = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
string clientId = secretClient.GetSecret("spn-appid").Value.Value;
string tenantId = secretClient.GetSecret("spn-tenantid").Value.Value;
string certificateName = "spn-certificate-Name";
string scope = "https://analysis.windows.net/powerbi/api/.default";
var certificateSecret = secretClient.GetSecret(certificateName);
var certificateBytes = Convert.FromBase64String(certificateSecret.Value.Value);
var cert = new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.MachineKeySet);
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithTenantId(tenantId)
.WithCertificate(cert)
.Build();
var result = app.AcquireTokenForClient(new string[] { scope })
.ExecuteAsync().Result;
Console.WriteLine(result.AccessToken);
}
}
Reference Links
Here are some of the reference links which might be useful more you to start your PowerBI embedding Journey.
Feel free to provide your comments, happy reading.