Embed Power BI Report In ASP.Net Core Website
The resource owner password credentials flow is not supported for the .Net core for Active Directory Authentication Library.
As a workaround, you can compose the HTTP request directly. Here is a example for your reference:
POST https://login.microsoftonline.com/{tenant}/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
&resource={resource}
&username={username}
&password={password}
&client_id={clientid}
&client_secret={client_secret}//if it is confidential app
Not sure how many more folks will come across this question in regards to adapting the PowerBI embedded App-Owns-Data sample code to .Net core, but I was able to successfully get things up and running by way of melding Chad's sample classes with the code from this post from the powerBI community.
The key (for me at least) was following the suggestion within the PowerBI post to swap this code in the original sample:
var credential = new UserPasswordCredential(Username, Password);
// Authenticate using created credentials
var authenticationContext = new AuthenticationContext(AuthorityUrl);
var authenticationResult = await
authenticationContext.AcquireTokenAsync(ResourceUrl, ClientId, credential);
with this bit (AAD being the returned object from Chad's sample):
var authenticationResult = AAD;
which also provides an actual embed token without calling UserPasswordCredential.
Full controller code below, hopefully it can save someone else a few hours of digging. Thanks Chad and Fei Xue for the sample! Cheers.
public class DashboardController : Controller
{
private static readonly string Username = "yourUN";
private static readonly string Password = "yourPW";
private static readonly string AuthorityUrl = "https://login.windows.net/common/oauth2/authorize/";
private static readonly string ResourceUrl = "https://analysis.windows.net/powerbi/api";
private static readonly string ClientId = "yourClientId";
private static readonly string ApiUrl = "https://api.powerbi.com";
private static readonly string GroupId = "yourGroupId";
private static readonly string ReportId = "yourReportId";
public class APIHelper
{
#region Settings
public static string BaseUrl
{
get
{
return "https://login.microsoftonline.com/common/oauth2/token";
}
}
#endregion
public static async Task<HttpResponseMessage> MakeAsyncRequest(string url, Dictionary<string, string> content)
{
var httpClient = new HttpClient
{
Timeout = new TimeSpan(0, 5, 0),
BaseAddress = new Uri(url)
};
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type: application/x-www-form-urlencoded", "application/json");
if (content == null)
{
content = new Dictionary<string, string>();
}
var encodedContent = new FormUrlEncodedContent(content);
var result = await httpClient.PostAsync(httpClient.BaseAddress, encodedContent);
return result;
}
}
public class AAD
{
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("scope")]
public string Scope { get; set; }
[JsonProperty("experies_in")]
public int ExpiresIn { get; set; }
[JsonProperty("ext_experies_in")]
public int ExtExpiresIn { get; set; }
[JsonProperty("experies_on")]
public int ExpiresOn { get; set; }
[JsonProperty("not_before")]
public int NotBefore { get; set; }
[JsonProperty("resource")]
public Uri Resource { get; set; }
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}
public async Task<ActionResult> Index(string username, string roles)
{
var url = APIHelper.BaseUrl;
var content = new Dictionary<string, string>();
content["grant_type"] = "password";
content["resource"] = "https://analysis.windows.net/powerbi/api";
content["username"] = Username;
content["password"] = Password;
content["client_id"] = ClientId;
var response = await APIHelper.MakeAsyncRequest(url, content);
var tokenresult = response.Content.ReadAsStringAsync().Result;
var AAD = JsonConvert.DeserializeObject<AAD>(tokenresult);
var result = new EmbedConfig();
try
{
result = new EmbedConfig { Username = username, Roles = roles };
var error = GetWebConfigErrors();
if (error != null)
{
result.ErrorMessage = error;
return View(result);
}
// Create a user password cradentials.
var authenticationResult = AAD;
if (authenticationResult == null)
{
result.ErrorMessage = "Authentication Failed.";
return View(result);
}
var tokenCredentials = new TokenCredentials(authenticationResult.AccessToken, "Bearer");
// Create a Power BI Client object. It will be used to call Power BI APIs.
using (var client = new PowerBIClient(new Uri(ApiUrl), tokenCredentials))
{
// Get a list of reports.
var reports = await client.Reports.GetReportsInGroupAsync(GroupId);
Report report;
if (string.IsNullOrEmpty(ReportId))
{
// Get the first report in the group.
report = reports.Value.FirstOrDefault();
}
else
{
report = reports.Value.FirstOrDefault(r => r.Id == "3a447f03-ad31-4e78-a3f1-2a6c008fcd8e");
}
if (report == null)
{
result.ErrorMessage = "Group has no reports.";
return View(result);
}
var datasets = await client.Datasets.GetDatasetByIdInGroupAsync(GroupId, report.DatasetId);
result.IsEffectiveIdentityRequired = datasets.IsEffectiveIdentityRequired;
result.IsEffectiveIdentityRolesRequired = datasets.IsEffectiveIdentityRolesRequired;
GenerateTokenRequest generateTokenRequestParameters;
// This is how you create embed token with effective identities
generateTokenRequestParameters = new GenerateTokenRequest(accessLevel: "view");
var tokenResponse = await client.Reports.GenerateTokenInGroupAsync(GroupId, report.Id, generateTokenRequestParameters);
if (tokenResponse == null)
{
result.ErrorMessage = "Failed to generate embed token.";
return View(result);
}
// Generate Embed Configuration.
result.EmbedToken = tokenResponse;
result.EmbedUrl = report.EmbedUrl;
result.Id = report.Id;
return View(result);
}
}
catch (HttpOperationException exc)
{
result.ErrorMessage = string.Format("Status: {0} for ID: ({1})\r\nResponse: {2}\r\nRequestId: {3}", exc.Response.StatusCode, (int)exc.Response.StatusCode, exc.Response.Content, exc.Response.Headers["RequestId"].FirstOrDefault());
}
catch (Exception exc)
{
result.ErrorMessage = exc.ToString();
}
return View(result);
}
}
I tried the solutions here and various other ones online for getting an embed token is aspnetcore. I kept getting invalid username/password, but I found that if I used the .NET Framework example from Microsoft everything worked just fine -
https://github.com/Microsoft/PowerBI-Developer-Samples/tree/master/App%20Owns%20Data/PowerBIEmbedded_AppOwnsData
So, I know my credentials were correct. Finally, I ran the .NET Framework solution locally and watched my outgoing traffic with Fiddler. I was able to see that there were additional exchanges happening because I was on a Federated AD tenant (ADFS). I worked backward from there to come up with the following (working) code. Hopefully it helps someone -
public class PowerBIAuthenticator
{
private static OAuthResult _cachedResult = null;
private PowerBIOptions _powerBIOptions;
private ILogger<PowerBIAuthenticator> _logger;
public PowerBIAuthenticator()
{
_powerBIOptions = new PowerBIOptions();
_powerBIOptions.ResourceUrl = "https://analysis.windows.net/powerbi/api";
_powerBIOptions.AuthorityUrl = "https://login.windows.net/common/oauth2/token";
_powerBIOptions.ApiUrl = "https://api.powerbi.com/";
_powerBIOptions.ClientId = "azure-ad-client-id-here";
_powerBIOptions.Username = "master-account-username-here";
_powerBIOptions.Password = "master-account-password-here";
}
public async Task<OAuthResult> AuthenticateAsync(PowerBISecureOptions secureOptions)
{
if (_cachedResult != null)
{
var expireDateTime = DateTimeOffset.FromUnixTimeSeconds(_cachedResult.ExpiresOn);
var currentDateTime = DateTimeOffset.Now.UtcDateTime;
if (currentDateTime < expireDateTime)
{
return _cachedResult;
}
}
OAuthResult authToken = await this.GetAuthToken();
_cachedResult = authToken;
return authToken;
}
private async Task<OAuthResult> GetAuthToken()
{
string commonRequestGuid = Guid.NewGuid().ToString();
OAuthResult oauthResult = null;
UserRealm userRealm = await this.GetUserRealm(commonRequestGuid);
if (userRealm.account_type.Equals("Federated"))
{
XmlDocument metadata
= await this.GetFederationMetadata(commonRequestGuid, userRealm.federation_metadata_url);
string trustBinding = GetFederatedUserTrustBinding(metadata);
XmlDocument trustDocument
= await this.GetFederatedUserTrust(commonRequestGuid, trustBinding);
var userAssertionNodes = trustDocument.GetElementsByTagName("saml:Assertion");
var userAssertionNode = userAssertionNodes[0].OuterXml;
using (var client = new HttpClient())
{
string tokenUri = "https://login.windows.net/common/oauth2/token";
var ua = new UserAssertion(
userAssertionNode,
"urn:ietf:params:oauth:grant-type:saml1_1-bearer",
Uri.EscapeDataString(_powerBIOptions.Username));
UTF8Encoding encoding = new UTF8Encoding();
Byte[] byteSource = encoding.GetBytes(ua.Assertion);
string base64ua = Convert.ToBase64String(byteSource);
var tokenForm = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("resource", _powerBIOptions.ResourceUrl),
new KeyValuePair<string, string>("client_id", _powerBIOptions.ClientId),
new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:saml1_1-bearer"),
new KeyValuePair<string, string>("assertion", base64ua),
new KeyValuePair<string, string>("scope", "openid"),
});
var tokenResult = await client.PostAsync(tokenUri, tokenForm);
var tokenContent = await tokenResult.Content.ReadAsStringAsync();
oauthResult = JsonConvert.DeserializeObject<OAuthResult>(tokenContent);
}
}
else
{
using (var client = new HttpClient())
{
var result = await client.PostAsync(_powerBIOptions.AuthorityUrl, new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("resource", _powerBIOptions.ResourceUrl),
new KeyValuePair<string, string>("client_id", _powerBIOptions.ClientId),
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", _powerBIOptions.Username),
new KeyValuePair<string, string>("password", _powerBIOptions.Password),
new KeyValuePair<string, string>("scope", "openid"),
}));
var tokenContent = await result.Content.ReadAsStringAsync();
oauthResult = JsonConvert.DeserializeObject<OAuthResult>(tokenContent);
}
}
return oauthResult;
}
private async Task<UserRealm> GetUserRealm(string commonRequestGuid)
{
UserRealm userRealm = new UserRealm();
using (var client = new HttpClient())
{
string userRealmUri = $"https://login.windows.net/common/UserRealm/{_powerBIOptions.Username}?api-version=1.0";
HttpRequestMessage realmRequest = new HttpRequestMessage(HttpMethod.Get, userRealmUri);
realmRequest.Headers.Add("Accept", "application/json");
realmRequest.Headers.Add("return-client-request-id", "true");
realmRequest.Headers.Add("client-request-id", commonRequestGuid);
HttpResponseMessage realmResponse = client.SendAsync(realmRequest).Result;
string realmString = await realmResponse.Content.ReadAsStringAsync();
userRealm = JsonConvert.DeserializeObject<UserRealm>(realmString);
}
return userRealm;
}
private async Task<XmlDocument> GetFederationMetadata(string commonRequestGuid, string adfsMetadataUri)
{
string metadataString = string.Empty;
using (var client = new HttpClient())
{
HttpRequestMessage metadataRequest = new HttpRequestMessage(HttpMethod.Get, adfsMetadataUri);
metadataRequest.Headers.Add("Accept", "application/json");
metadataRequest.Headers.Add("return-client-request-id", "true");
metadataRequest.Headers.Add("client-request-id", commonRequestGuid);
HttpResponseMessage metadataResponse = client.SendAsync(metadataRequest).Result;
metadataString = await metadataResponse.Content.ReadAsStringAsync();
}
XmlDocument metadataDoc = new XmlDocument();
metadataDoc.LoadXml(metadataString);
return metadataDoc;
}
private async Task<XmlDocument> GetFederatedUserTrust(string commonRequestGuid, string trustBindingUri)
{
string trustString = null;
using (var client = new HttpClient())
{
HttpRequestMessage trustRequest = new HttpRequestMessage(HttpMethod.Post, trustBindingUri);
trustRequest.Headers.Add("Accept", "application/json");
trustRequest.Headers.Add("return-client-request-id", "true");
trustRequest.Headers.Add("client-request-id", commonRequestGuid);
trustRequest.Headers.Add("SOAPAction", "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue");
DateTime now = DateTime.UtcNow;
DateTime then = now.AddMinutes(10);
string trustBody = __federatedUserTrustBody.
Replace("{messageGuid}", Guid.NewGuid().ToString()).
Replace("{adfsUserEndpoint}", trustBindingUri).
Replace("{SecurityCreated}", now.ToString("o")).
Replace("{SecurityExpires}", then.ToString("o")).
Replace("{tokenGuid}", Guid.NewGuid().ToString()).
Replace("{username}", _powerBIOptions.Username).
Replace("{password}", _powerBIOptions.Password);
trustRequest.Content = new StringContent(trustBody, Encoding.UTF8, "application/soap+xml");
HttpResponseMessage userTrustResponse = client.SendAsync(trustRequest).Result;
trustString = await userTrustResponse.Content.ReadAsStringAsync();
}
XmlDocument trustDocument = new XmlDocument();
trustDocument.LoadXml(trustString);
return trustDocument;
}
private string GetFederatedUserTrustBinding(XmlDocument metadata)
{
XmlNodeList services = metadata.GetElementsByTagName("wsdl:service");
List<XmlNode> ports = new List<XmlNode>();
foreach (XmlNode node in services[0])
{
if (node.Name.Equals("wsdl:port"))
ports.Add(node);
}
XmlNode trustPort = ports.FirstOrDefault(p => p.Attributes["name"] != null
&& p.Attributes["name"].Value.Equals("UserNameWSTrustBinding_IWSTrust13Async"));
XmlNode trustAddress = null;
foreach (XmlNode node in trustPort.ChildNodes)
{
if (node.Name.Equals("soap12:address"))
{
trustAddress = node;
break;
}
}
return trustAddress.Attributes["location"].Value;
}
public class UserRealm
{
public string ver { get; set; }
public string account_type { get; set; }
public string domain_name { get; set; }
public string federation_protocol { get; set; }
public string federation_metadata_url { get; set; }
public string federation_active_auth_url { get; set; }
public string cloud_instance_name { get; set; }
public string cloud_audience_urn { get; set; }
}
public class PowerBIOptions
{
[JsonProperty("resourceUrl")]
public string ResourceUrl { get; set; }
[JsonProperty("authorityUrl")]
public string AuthorityUrl { get; set; }
[JsonProperty("apiUrl")]
public string ApiUrl { get; set; }
[JsonProperty("clientId")]
public string ClientId { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("password")]
public string Password { get; set; }
}
public class OAuthResult
{
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("scope")]
public string Scope { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("ext_expires_in")]
public int ExtExpiresIn { get; set; }
[JsonProperty("expires_on")]
public int ExpiresOn { get; set; }
[JsonProperty("not_before")]
public int NotBefore { get; set; }
[JsonProperty("resource")]
public Uri Resource { get; set; }
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}
private string __federatedUserTrustBody =
@"<s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope' xmlns:a='http://www.w3.org/2005/08/addressing' xmlns:u='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'>
<s:Header>
<a:Action s:mustUnderstand='1'>http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
<a:messageID>urn:uuid:{messageGuid}</a:messageID>
<a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo>
<a:To s:mustUnderstand='1'>{adfsUserEndpoint}</a:To>
<o:Security s:mustUnderstand='1' xmlns:o='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'><u:Timestamp u:Id='_0'><u:Created>{SecurityCreated}</u:Created><u:Expires>{SecurityExpires}</u:Expires></u:Timestamp><o:UsernameToken u:Id='uuid-{tokenGuid}'><o:Username>{username}</o:Username><o:Password>{password}</o:Password></o:UsernameToken></o:Security>
</s:Header>
<s:Body>
<trust:RequestSecurityToken xmlns:trust='http://docs.oasis-open.org/ws-sx/ws-trust/200512'>
<wsp:AppliesTo xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy'>
<a:EndpointReference>
<a:Address>urn:federation:MicrosoftOnline</a:Address>
</a:EndpointReference>
</wsp:AppliesTo>
<trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
<trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
</trust:RequestSecurityToken>
</s:Body>
</s:Envelope>";
}