PowerBi - how to authenticate to app.powerbi.com silently - powerbi

I have tried the method outlined in the Microsoft docs
which involves creating an app in Active Directory and then having code something very similar to:
var authContextUrl = "https://login.windows.net/common/oauth2/authorize";
var authenticationContext = new AuthenticationContext(authContextUrl);
var redirectUri = "https://dev.powerbi.com/Apps/SignInRedirect";
var pp = new PlatformParameters(PromptBehavior.Auto);
var result = authenticationContext.AcquireTokenAsync(PowerBiApiResource, clientId, new Uri(redirectUri), pp).GetAwaiter().GetResult();
if (result == null)
{
throw new InvalidOperationException("Failed to obtain the PowerBI API token");
}
var token = result.AccessToken;
return token;
I got this code working but it always insisted on prompting for a username and password, which is a problem for a function app.
I have also tried the approach in the silent function specified here
https://community.powerbi.com/t5/Developer/Data-Refresh-by-using-API-Need-Steps/m-p/209371#M6614
static string getAccessTokenSilently()
{
HttpWebRequest request = System.Net.HttpWebRequest.CreateHttp("https://login.windows.net/common/oauth2/token");
//POST web request to create a datasource.
request.KeepAlive = true;
request.Method = "POST";
request.ContentLength = 0;
request.ContentType = "application/x-www-form-urlencoded";
//Add token to the request header
request.Headers.Add("Authorization", String.Format("Bearer {0}", token));
NameValueCollection parsedQueryString = HttpUtility.ParseQueryString(String.Empty);
parsedQueryString.Add("client_id", clientID);
parsedQueryString.Add("grant_type", "password");
parsedQueryString.Add("resource", resourceUri);
parsedQueryString.Add("username", username);
parsedQueryString.Add("password", password);
string postdata = parsedQueryString.ToString();
//POST web request
byte[] dataByteArray = System.Text.Encoding.ASCII.GetBytes(postdata); ;
request.ContentLength = dataByteArray.Length;
//Write JSON byte[] into a Stream
using (Stream writer = request.GetRequestStream())
{
writer.Write(dataByteArray, 0, dataByteArray.Length);
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
dynamic responseJson = JsonConvert.DeserializeObject<dynamic>(responseString);
return responseJson["access_token"];
}
}
This code doesn't work.
Also this has issues, although I haven't tried it:
https://learn.microsoft.com/en-us/power-bi/developer/get-azuread-access-token
There doesn't appear to be anything up to date available that works that explains how to do this. Does anyone know how?

This is the best I've got so far. I have to create the application in AD using https://dev.powerbi.com/apps and then login using a powerbi pro userid and password, using the following code:
public static string GetPowerBiAccessToken(string tenantId, string clientId, string userId, string password)
{
var url = $"https://login.windows.net/{tenantId}/oauth2/token";
var request = (HttpWebRequest)WebRequest.Create(url);
request.KeepAlive = true;
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
var dataToPost = new Dictionary<string,string>
{
{"client_id", clientId},
{"grant_type", "password"},
{"resource", PowerBiApiResource},
{"username", userId},
{"password", password},
{"redirect_uri", "https://dev.powerbi.com/Apps/SignInRedirect" }
};
var postData = string.Empty;
foreach (var item in dataToPost)
{
if (!string.IsNullOrEmpty(postData))
postData += "&";
postData += $"{item.Key}={item.Value}";
}
var dataByteArray = System.Text.Encoding.ASCII.GetBytes(postData);
request.ContentLength = dataByteArray.Length;
using (var writer = request.GetRequestStream())
{
writer.Write(dataByteArray, 0, dataByteArray.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
var responseJson = JsonConvert.DeserializeObject<dynamic>(responseString);
return responseJson["access_token"];
}
(P.S. I appreciate the request part of this code could be much better but having spent over 2 days trying to work this out, I'm just glad to have something that is working right now. Also the redirectUrl matches the one in my app, I'm not sure if its essential or not).
In the event of errors, I used Fiddler 4 to tell me exactly what the error was rather than just getting 400 or 401 in the app.

Related

PowerBI Embedded: DeleteUserAsAdminAsync returns unauthorized

Receiving an unauthorized response when trying to remove a user from a workspace (group). I'm using the .Net SDK method DeleteUserAsAdminAsync. I tried the DeleteUserInGroupAsync method with the same result. I've had no trouble retrieving groups, reports, even adding users to groups using the same authentication code shown below. I'm quite stumped.
public async Task<ResultObject> RemoveUsersFromWorkspaces(List<PowerBIWorkspace> powerBIWorkspaces)
{
// Authenticate using created credentials
AuthenticationResult authenticationResult = null;
authenticationResult = await DoAuthentication("CustomerAppRegistration");
var tokenCredentials =
new TokenCredentials(authenticationResult.AccessToken, "Bearer");
using (var client = new PowerBIClient(
new Uri("https://api.powerbi.com/"), tokenCredentials))
{
try
{
foreach (var wksp in powerBIWorkspaces)
{
// Remove the user to the workspace.
await client.Groups.DeleteUserAsAdminAsync(new Guid(wksp.WorkspaceId), wksp.UserEmail);
}
}
catch (Exception ex)
{
var errorObject = new ResultObject();
errorObject.Success = false;
errorObject.ErrorMessage = ex.Message;
return errorObject;
}
}
var resultObject = new ResultObject();
resultObject.Success = true;
resultObject.ErrorMessage = "";
return resultObject;
}
private const string AuthorityFormat = "https://login.microsoftonline.com/{0}/v2.0";
private const string MSGraphScope = "https://analysis.windows.net/powerbi/api/.default";
private async Task<AuthenticationResult> DoAuthentication(string appRegistrationSection)
{
var config = new ConfigurationBuilder()
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddJsonFile("appsettings.json").Build();
//var section = config.GetSection(nameof(AppRegistration));
var section = config.GetSection(appRegistrationSection);
var appRegistration = section.Get<AppRegistration>();
TenantID = appRegistration.TenantId;
ClientID = appRegistration.ClientId;
ClientSecret = appRegistration.ClientSecret;
IConfidentialClientApplication daemonClient;
daemonClient = ConfidentialClientApplicationBuilder.Create(ClientID)
.WithAuthority(string.Format(AuthorityFormat, TenantID))
.WithClientSecret(ClientSecret)
.Build();
AuthenticationResult authResult =
await daemonClient.AcquireTokenForClient(new[] { MSGraphScope }).ExecuteAsync();
return authResult;
}

How can I read result of web api call from Dynamics 365?

I try to retrieve a record from Dynamics 365 Sales. I created an app registration in Azure and I can get tokens based on this app.
Also, I can call the HTTP client. But I couldn't figure out how to read the result of the HTTP call.
Microsoft published only WhoAmIRequest sample, but I couldn't find a sample of other entities.
Here is my sample code. I try to read body object.
try
{
string serviceUrl = "https://****.crm4.dynamics.com/";
string clientId = "******";
string clientSecret = "*******";
string tenantId = "*******";
A***.Library.Utility.MSCRM mscrm = new Library.Utility.MSCRM(serviceUrl, clientId, clientSecret, tenantId);
var token = await mscrm.GetTokenAsync();
Console.WriteLine(token);
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(serviceUrl);
client.Timeout = new TimeSpan(0, 2, 0); //2 minutes
client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
client.DefaultRequestHeaders.Add("OData-Version", "4.0");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "/api/data/v9.0/accounts");
// Set the access token
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = client.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
// Get the response content and parse it.
var responseStr = response.Content.ReadAsStringAsync();
JObject body = JObject.Parse(response.Content.ReadAsStringAsync().Result);
}
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
Here is the result of body object.
You can use either of these syntax to read values. Read more
JObject body = JObject.Parse(response.Content.ReadAsStringAsync().Result);
// Can use either indexer or GetValue method (or a mix of two)
body.GetValue("obs_detailerconfigid");
body["obs_detailerconfigid"];

Connect to Azure Text Analytics from Console App without await

I am trying to call an Azure API (Text Analytics API) from a C# console application with a HttpRequest and I do not want to use any DLLs or await
but using the below snippet I am receiving "Bad Request". Can someone help me where it is going wrong.
public static void ProcessText()
{
string apiKey = "KEY FROM AZURE";
var client = new HttpClient();
var queryString = HttpUtility.ParseQueryString(string.Empty);
// Request headers
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiKey);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var requestUri = "https://eastus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment?" + queryString;
//HttpResponseMessage response;
// Request body
byte[] byteData = Encoding.UTF8.GetBytes("I really love Azure. It is the best cloud platform");
using (var content = new ByteArrayContent(byteData))
{
//content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = client.PostAsync(requestUri, content).Result;
Console.WriteLine(response);
Console.ReadLine();
}
}
string apiKey = "<<Key from Azure>>";
var client = new HttpClient();
var queryString = HttpUtility.ParseQueryString(string.Empty);
// Request headers
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiKey);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var requestUri = "https://**eastus**.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment?" + queryString;
//HttpResponseMessage response;
var body = new
{
documents = new[]
{
new
{
ID="1", text="I really love Azure. It is the best cloud platform"
}
}
};
string json = JsonConvert.SerializeObject(body);
byte[] byteData = Encoding.UTF8.GetBytes(json);
dynamic item = null;
using (var con = new ByteArrayContent(byteData))
{
//content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = client.PostAsync(requestUri, con).Result;
if (response.StatusCode == HttpStatusCode.OK)
{
string res = string.Empty;
using (HttpContent content = response.Content)
{
Task<string> result = content.ReadAsStringAsync();
res = result.Result;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
item = serializer.Deserialize<object>(res);
}
}
Hi All, I could able to get the API output using the above approach

PowerBI Embedded V2 Direct Query - Azure Sql credentials not updated programatically

I'm programatically creating a workspace and importing a Direct-Query report into PowerBI Embedded V2. Everything works fine except the report data source credentials which are not updated.
This is the code for the import flow:
await _powerBIService.ImportPbixAsync(newWorkspaceId, reportNameWithoutExtension, fileStream);
Console.WriteLine("Imported report {0} for {1} ({2})", reportNameWithoutExtension, persona.Name, persona.Id);
string datasetId = null;
while (datasetId == null)
{
//get DataSource Id
Thread.Sleep(5000);
datasetId = await _powerBIService.GetDatasetIdFromWorkspace(newWorkspaceId, reportNameWithoutExtension);
}
//update the connection details
await _powerBIService.UpdateConnectionAsync(newWorkspaceId, datasetId, persona.SQLUser, persona.SQLPassword);
Console.WriteLine("Updated connection details for dataset {0}", reportNameWithoutExtension);
// get gateway ID
var gatewayId = await _powerBIService.GetGatewayIdFromWorkspaceAndDataset(newWorkspaceId, datasetId);
if (gatewayId != null)
{
//update credentials
await _powerBIService.UpdateGatewayDatasourcesCredentials(gatewayId, persona.SQLUser, persona.SQLPassword);
Console.WriteLine("Updated connection details for gateway {0}", reportNameWithoutExtension);
}
These are the individual methods:
public async Task UpdateConnectionAsync(string workspaceId, string datasetId, string sqlUser, string sqlPwd)
{
var bearerToken = await GetBearerTokenAsync();
var tokenCredentials = new TokenCredentials(bearerToken, "Bearer");
using (var client = new PowerBIClient(new Uri(_pbiApiUrl), tokenCredentials))
{
var dataSourcesResponse = await client.Datasets.GetDatasourcesInGroupAsync(workspaceId, datasetId);
var sqlDataSources = dataSourcesResponse.Value?.Where(s => s.DatasourceType == "Sql")
.Where(s => !s.ConnectionString.Contains(_reportsSQLServer) || !s.ConnectionString.Contains(_reportsSQLServer));
foreach (var sqlDataSource in sqlDataSources)
{
var updateDataSourceConnectionRequest = new UpdateDatasourceConnectionRequest();
updateDataSourceConnectionRequest.ConnectionDetails = new DatasourceConnectionDetails(_reportsSQLServer, _reportsSQLDatabase);
//updateDataSourceConnectionRequest.DatasourceSelector = new Datasource(datasourceId: sqlDataSource.DatasourceId);
var datasourcesRequest = new UpdateDatasourcesRequest();
datasourcesRequest.UpdateDetails = new List<UpdateDatasourceConnectionRequest>() { updateDataSourceConnectionRequest };
var result = await client.Datasets.UpdateDatasourcesInGroupAsync(workspaceId, datasetId, datasourcesRequest);
//await client.Gateways.UpdateDatasourceAsync()
}
}
}
public async Task UpdateGatewayDatasourcesCredentials(string gatewayId, string sqlUser, string sqlPassword)
{
var bearerToken = await GetBearerTokenAsync();
var tokenCredentials = new TokenCredentials(bearerToken, "Bearer");
using (var client = new PowerBIClient(new Uri(_pbiApiUrl), tokenCredentials))
{
var datasourcesResult = await client.Gateways.GetDatasourcesAsync(gatewayId);
var sqlGatewayDatasources = datasourcesResult.Value?
.Where(s => s.DatasourceType == "Sql")
.Where(s => s.ConnectionDetails.Contains(_reportsSQLServer) && s.ConnectionDetails.Contains(_reportsSQLDatabase));
foreach (var gatewayDatasource in sqlGatewayDatasources)
{
var updateDataSourceRequest = new UpdateDatasourceRequest();
updateDataSourceRequest.CredentialDetails = new CredentialDetails();
updateDataSourceRequest.CredentialDetails.CredentialType = "Basic";
updateDataSourceRequest.CredentialDetails.Credentials = "{\"credentialData\":[{\"name\":\"username\", \"value\":\"" + sqlUser + "\"},{\"name\":\"password\", \"value\":\"" + sqlPassword + "\"}]}";
updateDataSourceRequest.CredentialDetails.EncryptedConnection = "Encrypted";
updateDataSourceRequest.CredentialDetails.EncryptionAlgorithm = "None";
updateDataSourceRequest.CredentialDetails.PrivacyLevel = "None";
var result = await client.Gateways.UpdateDatasourceAsync(gatewayId, gatewayDatasource.Id, updateDataSourceRequest);
}
}
}
When I attempt to view the report there is no data being fetched from the database.
Interestingly I can login manually using the PowerBI portal using the same credentials. After the manual login the report is fetching data.
If after the manual login I delete the workspace and then recreate and re-import the report programatically then the report will show data. This makes me think there is some caching involved after the manual logic which is persisted outside of the workspace.
Any suggestions on what is missing here?

Call a UI controller with FedAuth cookie from SAML token

We're trying to automate some integrated tests. So we'd like to be able to programatically call the UI controllers in order to be as clause as what the reel users do. We don't want to use a UI test suite for different reasons.
Problem is that we are using SSO Windows authentication with the WS-Federation security with WIF. In configuration, we use passiveRedirectEnabled="true" so that every time the session cookie is absent, invalid or expired, the page gets redirected to the AD FS STS endpoint ("/adfs/ls/"). The result is again redirected back to page specify in the "reply" attribute in the Web.config file.
When I look in Fiddler, I clearly see the second redirect (coming back from the AD FS STS) with a 302 status returns a "Set-Cookie : FedAuth=77u/PD94bWwg..." instruction to the browser. The the call is made to the reply page with the FedAuth cookie and everything is OK from there.
Is there a way to emulate this behavior and be able to call the UI controller with the correct FedAuth cookie ? No SharePoint please, this has nothing to do with it.
I was finally able to reproduce the steps from what I saw in Fiddler to mimic the browser. I'll let the code here, hoping it can help some of you along the way. It's not very clean, it's more in a POC mode but it still can help. Note that on some requests I had to allow the automatic redirection an some others I had to prevent it.
Credits to my colleague Dominique Pothier who helped me a lot on that one.
//First request to the secured site
var request =
(HttpWebRequest)WebRequest.Create("https://mysite.mycompany.ca/");
request.Method = "GET";
request.UseDefaultCredentials = true;
request.PreAuthenticate = true;
request.AllowAutoRedirect = false;
var httpResponse = (HttpWebResponse)request.GetResponse();
//Redirects to the STS based on the response from the first call, posting the ws-federations infos along
request =
(HttpWebRequest)WebRequest.Create(httpResponse.Headers["Location"]);
request.UseDefaultCredentials = true;
request.PreAuthenticate = true;
request.Host = "sts.mycompany.ca";
request.AllowAutoRedirect = true;
request.UserAgent =
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36";
request.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
var nameValueCollection = new NameValueCollection { { "Cache-Control", "max-age=0" } };
request.Headers.Add(nameValueCollection);
nameValueCollection = new NameValueCollection { { "Upgrade-Insecure-Requests", "1" } };
request.Headers.Add(nameValueCollection);
nameValueCollection = new NameValueCollection { { "Accept-Encoding", "gzip, deflate, sdch" } };
request.Headers.Add(nameValueCollection);
nameValueCollection = new NameValueCollection { { "Accept-Language", "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4" } };
request.Headers.Add(nameValueCollection);
httpResponse = (HttpWebResponse)request.GetResponse();
//Parse the response to get ws-federation infos
var responseStream = new StreamReader(httpResponse.GetResponseStream());
var responseData = responseStream.ReadToEnd();
var xmlReader = XmlReader.Create(new StringReader(responseData));
var wa = "";
var wresult = "";
var wctx = "";
while (xmlReader.Read())
{
if (xmlReader.GetAttribute("name") == "wa")
wa = xmlReader.GetAttribute("value");
if (xmlReader.GetAttribute("name") == "wresult")
wresult = xmlReader.GetAttribute("value");
if (xmlReader.GetAttribute("name") == "wctx")
wctx = xmlReader.GetAttribute("value");
}
httpResponse.Close();
//Redirects to the controller method we want to hit
request =
(HttpWebRequest)WebRequest.Create("https://mysite.mycompany.ca/Home/GetStates");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.AllowAutoRedirect = false;
//Add the cookie container to the response so that we can get the FedAuth cookie after the response
request.CookieContainer = new CookieContainer();
//Add the ws-federation infos from the last http request to the body of the new request
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
if (wa != null)
{
var waEncoded = HttpUtility.UrlEncode(wa);
var wresultEncoded = HttpUtility.UrlEncode(wresult);
var wctxEncoded = HttpUtility.UrlEncode(wctx);
var urlEncoded = "wa=" + waEncoded + "&wresult=" + wresultEncoded + "&wctx=" + wctxEncoded;
streamWriter.Write(urlEncoded);
streamWriter.Flush();
streamWriter.Close();
}
}
request.Referer = httpResponse.ResponseUri.OriginalString;
httpResponse = (HttpWebResponse)request.GetResponse();
var cookieContainer = request.CookieContainer;
//Use the FedAuth cookie that we got from last http call and add it to a new request to the controller and voila !
request =
(HttpWebRequest)WebRequest.Create("https://mysite.mycompany.ca/Home/GetStates");
request.Method = "GET";
nameValueCollection = new NameValueCollection { { "X-Requested-With", "XMLHttpRequest" } };
request.Headers.Add(nameValueCollection);
//Add the FedAuthCookie from last request
request.CookieContainer = cookieContainer;
request.Referer = "https://proacces-dev1.universitas.ca/";
httpResponse = (HttpWebResponse)request.GetResponse();
responseStream = new StreamReader(httpResponse.GetResponseStream());
responseData = responseStream.ReadToEnd();
Console.WriteLine(responseData);
Console.ReadLine();