I am writing an App server application in C# that needs to access Firebase Database. It uses REST protocol. To authentication i want to use an service account.
Unfortunately there is no library written in C#, so i am trying to put the bellow http Request to work.
I follow this steps:
To get the accesstoken i follow the https://github.com/google/google-api-dotnet-client-samples. The code prints the token so should be ok to that point.
Invoke GET web request passing the token in the access_token query parameter as documented at https://firebase.google.com/docs/reference/rest/database/user-auth.
I tried all variations i could remember, in headers, with apostrophe, APN request style, but always got 401 error or 403. Error code 403 should mean that the API recognize the user but denys access to the resource, but i am not sure if this works this way in this case.
The account is defined in the API console and it has project edit and owner profile, for the Firebase app.
The rules are set like this:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Can't figure out were i went wrong. I don't think i need to go written an JWT token if i use google API library. Rules should not apply to this account so i guess i am not passing the token correctly. By inspecting the token retrieved i can see that it is of type Bear, so i tried to pass it on header with no success too.
Test code:
using System;
using System.Security.Cryptography.X509Certificates;
using Google.Apis.Auth.OAuth2;
using System.Threading.Tasks;
using System.Net;
using System.IO;
namespace FirebaseAppServer
{
/// </summary>
public class Program
{
public static void Main(string[] args)
{
accessFirebase();
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
public async static Task accessFirebase()
{
String serviceAccountEmail = "serviceaccount1#myapp.iam.gserviceaccount.com";
var certificate = new X509Certificate2(#"App.p12", "notasecret", X509KeyStorageFlags.Exportable); //App2 is the certificate i downloaded from API console
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { "https://www.googleapis.com/auth/firebase.database" //from https://developers.google.com/identity/protocols/googlescopes
,"https://www.googleapis.com/auth/firebase"
,"https://www.googleapis.com/auth/cloud-platform"}
}.FromCertificate(certificate));
var task = await credential.RequestAccessTokenAsync(System.Threading.CancellationToken.None);
Console.WriteLine("AccessToken " + credential.Token.AccessToken); //accessToken has a value, so guess is all good so far.
var request = (HttpWebRequest)WebRequest.Create("https://<Myapp>.firebaseio.com/.json?access_token=" + credential.Token.AccessToken);
request.Method = "GET";
request.ContentType = "application/json";
using (var response = (HttpWebResponse)request.GetResponse()) //Throw error 403 - forbidden
{
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
Console.WriteLine("responseString " + responseString);
}
}
Related
I have been trying WITH NO LUCK, to get an embed token to be able to embed my powerbi reports into my existing .netcore web api application. The front end looks like a super easy 1 simple react component that power bi has prepared for me.
But for the backend, I'm literally going in circles.
I got to the point where I decided the cleanest way for me to do this would be through an HTTP Trigger function.
(see this: https://www.taygan.co/blog/2018/05/14/embedded-analytics-with-power-bi )
As an important side note: I DID already grant my application the necessary delegate READ permissions to the power bi Apis)
Another side note, is that I am attempting to do the master user, app owns data approach
Another side note, is that you will see that my link above, the code shows you to get an AAD auth token using a method that is no longer supported (seemingly) by microsoft, so I changed that line of code as you'll see below
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.PowerBI.Api;
using Microsoft.PowerBI.Api.Models;
using Microsoft.Rest;
using Newtonsoft.Json;
namespace PowerBiExample
{
public class EmbedContent
{
public string EmbedToken { get; set; }
public string EmbedUrl { get; set; }
public string ReportId { get; set; }
}
public static class Test
{
private static string tenantId = "this is the id of my entire organization";
static string authorityUrl = $"https://login.microsoftonline.com/{tenantId}";
static string resourceUrl = "https://analysis.windows.net/powerbi/api";
static string apiUrl = "https://api.powerbi.com/";
private static string clientId = "this is the client id of my application that i gave delegate permissions to";
private static string clientSecret = "this is the secret of the application i gave delegate permissions to";
private static string username = "ad master user that i WANTED to sign into power bi with";
private static string password = "that ad users pw";
private static Guid groupId = Guid.Parse("workspaceid in powerbi");
private static Guid reportId = Guid.Parse("report id from within that workspace");
[FunctionName("Test")]
public static async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
var credential = new ClientCredential(clientId, clientSecret);
var authenticationContext = new AuthenticationContext(authorityUrl);
// var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, clientId, credential);
var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, credential);
string accessToken = authenticationResult.AccessToken;
var tokenCredentials = new TokenCredentials(accessToken, "Bearer");
using (var client = new PowerBIClient(new Uri(apiUrl), tokenCredentials))
{
// Embed URL
Report report = client.Reports.GetReportInGroup(groupId, reportId);
string embedUrl = report.EmbedUrl;
// Embed Token
var generateTokenRequestParameters = new GenerateTokenRequest(accessLevel: "view");
EmbedToken embedToken = client.Reports.GenerateTokenInGroup(groupId, reportId, generateTokenRequestParameters);
// JSON Response
EmbedContent data = new EmbedContent();
data.EmbedToken = embedToken.Token;
data.EmbedUrl = embedUrl;
data.ReportId = reportId.ToString();
var s = JsonConvert.SerializeObject(data);
return new JsonResult(s);
// JavaScriptSerializer js = new JavaScriptSerializer();
// string jsonp = "callback(" + js.Serialize(data) + ");";
//
// // Return Response
// return new HttpResponseMessage(HttpStatusCode.OK)
// {
// Content = new StringContent(jsonp, Encoding.UTF8, "application/json")
// };
}
}
}
}
I DO get the Authorization Token returned. I DO NOT get the Embed Token returned. I get unauthorized for that.
Also important note: 1. I also didn't enable Service Principal like it says to do here learn.microsoft.com/en-us/power-bi/enterprise/… (my IT dept said I can't). and 2. they are not an admin or member on the workspace but when I try to add them as member, they are not available. It's an Application, not a user or a group. What should I do
Please check if below points can give an idea to work around.
A fiddler trace may be required to investigate further. The required
permission scope may be missing for the registered application
within Azure AD. Verify the required scope is present within the
app registration for Azure AD within the Azure portal ex: openid ,
profile, offline etc depending on the requirement and the user
logged in.
When using a master user, you'll need to define your app's delegated
permissions ( known as scopes). The master user or Power BI
admin is required to grant consent for using these permissions using
the Power BI REST APIs.
For master users, it is essential to grant permissions from the
Azure portal.
Also check if the group Id provided is correct,
Also For Analysis Services, the master user has to be a
gateway admin.
Your web app uses a user account to authenticate against Azure AD
and get the Azure AD token. The master user needs to have a Power
BI Pro or a Premium Per User (PPU) license.
After successful authentication against Azure AD, your web app will
generate an embed token to allow its users to access specific
Power BI content.
Please go through this considerations to generate embed token
carefully and give proper permissions.
Note: For security reasons, the lifetime of the embed token is set to the remaining lifetime of the Azure AD token used to call the
GenerateToken API.
Therefore, if you use the same Azure AD token to generate several embed tokens, the lifetime of the generated embed tokens will be shorter with each call.
Sometimes that can be the reason for the unauthorized error due to expiry of embed token
Please check this powerbi-docs/embedded-troubleshoot.md at live ·
MicrosoftDocs/powerbi-docs · GitHub to see several issues and
find what is the main issue in your case.
And also check if you need to provide datasetId
Some references:
Unauthorized response on GetReportInGroupAsync PowerBI Embedded
API call using Service Principal - Stack Overflow
Register an app to embed Power BI content in a Power BI embedded
analytics application - Power BI | Microsoft Docs
Understand the permission tokens needed for embedding a Power BI
application - Power BI | Microsoft Docs
I'm trying to implement a C# program to connect to Sharepoint API through modern authentication (Client ID\ Client Secret).
I've registered an APP with Sharepoint overall permissions on Azure Active Directory, in order to generate Client Id and Client Secret.
Next steps should be retrieval of the Access Token from the Microsoft login page, and then construction of all following requests using the bearing token I've generated.
Retrieval of the Access Token just works fine. The problem is when I try to include the token in the authorization header on the following calls.
I always get 401 Unhautorized when building my requests from code. Debugging the response content, what I get is "x-ms-diagnostics: 3000006;reason="Token contains invalid signature"; category"invalid_client".
Instead if I try to replicate the call in Postman I get the following error "{"error_description":"Unsupported security token."}".
I provide my code below. Does anybody knows what is going on?
var b2cAuthUri = "https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token";
var client = new HttpClient();
var dict = new Dictionary<string, string>();
dict.Add("Content-Type", "application/x-www-form-urlencoded");
dict.Add("grant_type", "client_credentials");
dict.Add("client_id", clientId);
dict.Add("client_secret", clientSecret);
dict.Add("scope", scope);
// Execute post method
using (var methodResp = client.PostAsync(b2cAuthUri, new FormUrlEncodedContent(dict)))
{
var callResult = methodResp.Result.Content.ReadAsStringAsync().Result;
if (!string.IsNullOrEmpty(callResult))
{
//I have my Access Token here :)
using (MemoryStream DeSerializememoryStream = new MemoryStream())
{
//initialize DataContractJsonSerializer object and pass custom token class type to it
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(AccessToken));
//user stream writer to write JSON string data to memory stream
StreamWriter writer = new StreamWriter(DeSerializememoryStream);
writer.Write(callResult);
writer.Flush();
DeSerializememoryStream.Position = 0;
//get the Desrialized data in object of type Student
AccessToken SerializedObject = (AccessToken)serializer.ReadObject(DeSerializememoryStream);
var tokenBytes = System.Text.Encoding.UTF8.GetBytes(SerializedObject.access_token);
//64bit serialized token
var tokenBase64 = System.Convert.ToBase64String(tokenBytes);
//Here I try to make a call with the access token as header
var testURI = "https://myorg.sharepoint.com/sites/crmkb/_api/web/lists";
HttpWebRequest testReq = (HttpWebRequest)HttpWebRequest.Create(testURI);
testReq.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + tokenBase64);
testReq.Method = "GET";
//This fails on 401 code
HttpWebResponse response = (HttpWebResponse)testReq.GetResponse();
}
}
}
SharePoint Online has blocked the Azure AD App Client Secret, so if you want to use Azure AD App to authentication with SharePoint Rest API, it's necessary to use Certificate option:
Calling SharePoint Online APIs using Azure AD App-Only permissions and certificate auth
Another option is to use the SharePoint hosted App Id/ Secret registered in "/_layouts/15/appregnew.aspx", this way supported the Client Secret, please check the demo test in Postman:
Accessing SharePoint Data using Postman (SharePoint REST API)
Problem
When I call "Request Sync" on the Google HomeGraph API I receive a "403 Forbidden" response.
Background
I'm writing a Smart Home Action, and have successfully implemented SYNC, QUERY and EXECUTE. Testing on my mobile I can see and interact with devices okay. I'm now trying to implement Request Sync, but can't appear to interact with the API. I am making what seems to be successful requests for an Access Token. The token always begins with "ya29.c." which in my naïve understanding suggests an empty header and payload (trying it on https://jwt.io). However, when testing it at https://accounts.google.com/o/oauth2/tokeninfo?access_token= it appears valid, showing both my service account unique ID and the scope I intended. When I make a call to the API, either manually posting the data, or via Google's own code, it gives me a blunt 403 error. I do not know where I can get any more information on this error other than the exception objects. I'm new to GCP and couldn't find any sort of log. Given I've tried different methods and all return a 403 I'm inclined to suspect the issue is more a account or credential-related than the code, but can't be certain.
API Key
(I'm no longer able to reproduce any errors relating to API keys being missing or invalid).
Although the documentation doesn't show it, I've seen some people use an API key. When I don't include the API key with a p12 certificate, or include an incorrect one it errors (either with API key missing, or API key invalid accordingly). I have created an unrestricted API key in IAM, and am using that. I can't appear to explicitly relate this to HomeGraph API, but it says that it can call any API.
Code
This example fetches an access token, then tries to call the API via POST with and without the API key. It then tries to authenticate and call the API via the Google library code. Each fails with a 403.
using Google;
using Google.Apis.Auth.OAuth2;
using Google.Apis.HomeGraphService.v1;
using Google.Apis.HomeGraphService.v1.Data;
using Google.Apis.Services;
using Lambda.Core.Constants;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using static Google.Apis.HomeGraphService.v1.DevicesResource;
public class Example
{
public void RequestSync()
{
const string UrlWithoutKey = #"https://homegraph.googleapis.com/v1/devices:requestSync";
const string UrlWithKey = #"https://homegraph.googleapis.com/v1/devices:requestSync?key=" + OAuthConstants.GoogleApiKey;
string accessToken = this.GetAccessToken();
// Manual Attempt 1
try
{
string response = this.CallRequestSyncApiManually(accessToken, UrlWithoutKey);
}
catch (WebException ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
// Manual Attempt 2
try
{
string response = this.CallRequestSyncApiManually(accessToken, UrlWithKey);
}
catch (WebException ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
// SDK Attempt
try
{
this.CallRequestSyncApiWithSdk();
}
catch (GoogleApiException ex)
{
// Google.Apis.Requests.RequestError
// The caller does not have permission[403]
// Errors[Message[The caller does not have permission] Location[- ] Reason[forbidden] Domain[global]]
// at Google.Apis.Requests.ClientServiceRequest`1.ParseResponse(HttpResponseMessage response) in Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 243
// at Google.Apis.Requests.ClientServiceRequest`1.Execute() in Src\Support\Google.Apis\Requests\ClientServiceRequest.cs:line 167
string msg = ex.Message;
}
}
private string GetAccessToken()
{
string defaultScope = "https://www.googleapis.com/auth/homegraph";
string serviceAccount = OAuthConstants.GoogleServiceAccountEmail; // "??????#??????.iam.gserviceaccount.com"
string certificateFile = OAuthConstants.CertificateFileName; // "??????.p12"
var oAuth2 = new GoogleOAuth2(defaultScope, serviceAccount, certificateFile); // As per https://stackoverflow.com/questions/26478694/how-to-produce-jwt-with-google-oauth2-compatible-algorithm-rsa-sha-256-using-sys
bool status = oAuth2.RequestAccessTokenAsync().Result;
// This access token at a glance appears invalid due to an empty header and payload,
// But verifies ok when tested here: https://accounts.google.com/o/oauth2/tokeninfo?access_token=
return oAuth2.AccessToken;
}
private string CallRequestSyncApiManually(string accessToken, string url)
{
string apiRequestBody = #"{""agentUserId"": """ + OAuthConstants.TestAgentUserId + #"""}";
var client = new HttpClient();
var request = (HttpWebRequest)WebRequest.Create(url);
var data = Encoding.ASCII.GetBytes(apiRequestBody);
request.Method = "POST";
request.Accept = "application/json";
request.ContentType = "application/json";
request.ContentLength = data.Length;
request.Headers.Add("Authorization", $"Bearer {accessToken}");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
return responseString;
}
private void CallRequestSyncApiWithSdk()
{
var certificate = new X509Certificate2(OAuthConstants.CertificateFileName, OAuthConstants.CertSecret, X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(OAuthConstants.GoogleServiceAccountEmail)
{
Scopes = new[] { "https://www.googleapis.com/auth/homegraph" },
}.FromCertificate(certificate));
var service = new HomeGraphServiceService(
new BaseClientService.Initializer()
{
// Complains if API key is not provided, even though we're using a certificate from a Service Account
ApiKey = OAuthConstants.GoogleApiKey,
HttpClientInitializer = credential,
ApplicationName = OAuthConstants.ApplicationName,
});
var request = new RequestSyncRequest(
service,
new RequestSyncDevicesRequest
{
AgentUserId = OAuthConstants.TestAgentUserId
});
request.Execute();
}
}
Account Configuration
Account screenshots. (I'm not allowed to post images yet, so they're links)
HomeGraph is enabled
My API Key is unrestricted
My Service Account has Owner & Service Account Token Creator enabled
Updates
I have tried skipping manually obtaining the access token, as per Devunwired's suggestion. Whilst this does eliminate the error I was getting from not providing the API key, I still end up with the 403. My reasoning for doing the access token part manually was part of debugging a 403 I was getting with the API call. That way I could at least see part of the process working. I'm happy to use the library version for the solution as the access token doesn't appear to be the issue.
public void GoogleLibraryJsonCredentialExample()
{
try
{
GoogleCredential credential;
using (var stream = new FileStream(OAuthConstants.JsonCredentialsFileName, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream).CreateScoped(new[] { OAuthConstants.GoogleScope });
}
var service = new HomeGraphServiceService(
new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = OAuthConstants.ApplicationName,
});
var request = new RequestSyncRequest(
service,
new RequestSyncDevicesRequest
{
AgentUserId = OAuthConstants.TestAgentUserId
});
request.Execute();
}
catch (Exception ex)
{
// Receive 403, Forbidden
string msg = ex.Message;
}
}
Concerns
Is it possible that I need to be making the API call from a verified or white-listed domain? At the moment I'm running it from a console app running on my development machine. My understanding of domain verification is that it does not apply to incoming calls, and therefore shouldn't be the problem.
I am making what seems to be successful requests for an Access Token.
You should not need to manually request OAuth access tokens when using the Google client libraries. They generally handle this process internally using the credentials you provide from the GCP console.
Although the documentation doesn't show it, I've seen some people use an API key. Indeed, it is mandatory to include it for the SDK approach.
We do not recommend using the API key method to access the Home Graph API. You should be using service account credentials. API keys will technically work for the Request Sync method, but you will not be able to authenticate Report State using an API key.
The fact that you are receiving an error trying to build the HomeGraphServiceService without an API key may be indicative that the credential you are using isn't set up correctly (no private key or possibly missing scopes). The recommended method for supplying service account credentials is to download them in the JSON format rather than certificate, and the code to generate a credential from JSON should look something like this:
GoogleCredential credential;
using (var stream = new FileStream(serviceAccountCredentialFilePath, FileMode.Open, FileAccess.Read))
{
credential = GoogleCredential.FromStream(stream).CreateScoped(scopes);
}
You can find additional C# examples for authenticating APIs in the authentication guide.
The problem was nothing to do with my permission to talk to the HomeGraph API or that user. Instead it was where HomeGraph wanted to call my Smart Home Action, but the access token had expired. When attempting to refresh the token, an erroneous implementation on my part led to a blunt 403, which Google was then relaying back to me.
For those interested, the issue was that rather than omitting the expiry date for a token that should never expire, I was setting it to DateTime.MaxValue (subsequently sent through some further processing). Unfortunately when this is finally cast to an int, it is a value that exceeds int.Max. The subsequent time on the expiry was set to epoch (i.e. in the past), and therefore the token validation failed due to expiry.
For anyone else still having the same issue, double check your agentUserId matches exactly the value shown in your SYNC output payload. In my case I'd checked this.
Many thanks to anyone who's looked at this.
I would like to programmatically list and control virtual machines classic (old one) in Azure. For managed it is not problem, there are libraries and the rest API is working, but once I am calling the old API for listing classic, I got 403 (Forbidden).
Is the code fine? Do I need to manage credentials for old API on another place?
My code is here:
static void Main(string[] args)
{
string apiNew = "https://management.azure.com/subscriptions/xxxxxxxxxxxxxxxxxxxxxxxx/providers/Microsoft.Compute/virtualMachines?api-version=2018-06-01";
string apiOld = "https://management.core.windows.net/xxxxxxxxxxxxxxxxxxxxxxxx/services/vmimages"
AzureRestClient client = new AzureRestClient(credentials.TenantId, credentials.ClientId, credentials.ClientSecret);
//OK - I can list the managed VMs.
string resultNew = client.GetRequestAsync(apiNew).Result;
// 403 forbidden
string resultOld = client.GetRequestAsync(apiOld).Result;
}
public class AzureRestClient : IDisposable
{
private readonly HttpClient _client;
public AzureRestClient(string tenantName, string clientId, string clientSecret)
{
_client = CreateClient(tenantName, clientId, clientSecret).Result;
}
private async Task<string> GetAccessToken(string tenantName, string clientId, string clientSecret)
{
string authString = "https://login.microsoftonline.com/" + tenantName;
string resourceUrl = "https://management.core.windows.net/";
var authenticationContext = new AuthenticationContext(authString, false);
var clientCred = new ClientCredential(clientId, clientSecret);
var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, clientCred);
var token = authenticationResult.AccessToken;
return token;
}
async Task<HttpClient> CreateClient(string tenantName, string clientId, string clientSecret)
{
string token = await GetAccessToken(tenantName, clientId, clientSecret);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return client;
}
public async Task<string> GetRequestAsync(string url)
{
return await _client.GetStringAsync(url);
}
}
UPDATE 1:
Response details:
HTTP/1.1 403 Forbidden
Content-Length: 288
Content-Type: application/xml; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Mon, 22 Oct 2018 11:03:40 GMT
HTTP/1.1 403 Forbidden
Content-Length: 288
Content-Type: application/xml; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Mon, 22 Oct 2018 11:03:40 GMT
<Error xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Code>ForbiddenError</Code>
<Message>The server failed to authenticate the request.
Verify that the certificate is valid and is associated with this subscription.</Message>
</Error>
Update 2:
I found that same API is used by powershell command Get-AzureVMImage and it is working from powershell. Powershell ask me first to login to Azure with interactive login windows by email and password and the the request use Bearer header to authenticate like mine code.
If I sniff the access token (Bearer header) from communication created by Powershell, I can communicate with that API with success.
Update 3: SOLVED, answer bellow.
1. Reason for 403 when you're calling List VM Images API
It's because your Azure AD registered application is not using the "Windows Azure Service Management API" delegated permissions correctly. I say this because I see your code is acquiring the token directly using application identity (ClientCredential) and not as a user.
Please see the screenshots below. Window Azure Service Management API clearly does not provide any application permissions, only thing that can be used is a delegated permission. If you want to understand more about the difference between the two kinds of permissions, read Permissions in Azure AD. To put it very briefly, when using delegated permissions, the app is delegated permission to act as the signed-in user when making calls to an API. So there has to be a signed-in user.
I was able to reproduce the 403 error using your code and then able to make it work and return a list of classic VM's with some changes. I'll explain the required changes next.
Go to your Azure AD > App registrations > your app > Settings > Required permissions :
2. Changes required to make it work
Change will be to acquire token as a signed in user and not directly using application's clientId and secret. Since your application is a console app, it would make sense to do something like this, which will prompt the user to enter credentials:
var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto));
Also, since your application is a console application, it would be better to register it as a "Native" application instead of a web application like you have it right now. I say this because console applications or desktop client based applications which can run on user systems are not secure to handle application secrets, so you should not register them as "Web app / API" and not use any secrets in them as it's a security risk.
So overall, 2 changes and you should be good to go. As I said earlier, I have tried these and can see the code working fine and getting a list of classic VMs.
a. Register your application in Azure AD as a native app (i.e. Application Type should be native and not Web app / API), then in required permissions add the "Window Azure Service Management API" and check the delegated permissions as per earlier screenshots in point 1
b. Change the way to acquire token, so that delegated permissions can be used as per the signed in user. Of course, signed in user should have permissions to the VM's you're trying to list or if you have multiple users, the list will reflect those VM's which currently signed in user has access to.
Here is the entire working code after I modified it.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Net.Http;
using System.Net.Http.Headers;
namespace ListVMsConsoleApp
{
class Program
{
static void Main(string[] args)
{
string tenantId = "xxxxxx";
string clientId = "xxxxxx";
string redirectUri = "https://ListClassicVMsApp";
string apiNew = "https://management.azure.com/subscriptions/xxxxxxxx/providers/Microsoft.Compute/virtualMachines?api-version=2018-06-01";
string apiOld = "https://management.core.windows.net/xxxxxxxx/services/vmimages";
AzureRestClient client = new AzureRestClient(tenantId, clientId, redirectUri);
//OK - I can list the managed VMs.
//string resultNew = client.GetRequestAsync(apiNew).Result;
// 403 forbidden - should work now
string resultOld = client.GetRequestAsync(apiOld).Result;
}
}
public class AzureRestClient
{
private readonly HttpClient _client;
public AzureRestClient(string tenantName, string clientId, string redirectUri)
{
_client = CreateClient(tenantName, clientId, redirectUri).Result;
}
private async Task<string> GetAccessToken(string tenantName, string clientId, string redirectUri)
{
string authString = "https://login.microsoftonline.com/" + tenantName;
string resourceUrl = "https://management.core.windows.net/";
var authenticationContext = new AuthenticationContext(authString, false);
var authenticationResult = await authenticationContext.AcquireTokenAsync(resourceUrl, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto));
return authenticationResult.AccessToken;
}
async Task<HttpClient> CreateClient(string tenantName, string clientId, string redirectUri)
{
string token = await GetAccessToken(tenantName, clientId, redirectUri);
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
client.DefaultRequestHeaders.Add("x-ms-version", "2014-02-01");
return client;
}
public async Task<string> GetRequestAsync(string url)
{
return await _client.GetStringAsync(url);
}
}
}
According to the linked documentation you appear to be missing a required request header when requesting the classic REST API
x-ms-version - Required. Specifies the version of the operation to use for this request. This header should be set to 2014-02-01 or higher.
Reference List VM Images: Request Headers
To allow for the inclusion of the header, create an overload for GET requests in the AzureRestClient
public async Task<string> GetRequestAsync(string url, Dictionary<string, string> headers) {
var request = new HttpRequestMessage(HttpMethod.Get, url);
if (headers != null)
foreach (var header in headers) {
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
var response = await _client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
and include the required header when calling apiOld
var headers = new Dictionary<string, string>();
headers["x-ms-version"] = "2014-02-01";
string resultOld = client.GetRequestAsync(apiOld, headers).GetAwaiter().GetResult();
Finnaly I got it to work:
First Open Powershell:
Get-AzurePublishSettingsFile
and save that file.
then type in Powershell
Import-AzurePublishSettingsFile [mypublishsettingsfile]
Open certificate store and find imported certificate. And use that certificate
at the same time with credentials within the HttpClient.
Based on my test, you need to get the access token interactively.
I've perfectly reproduced your issue.
Unfortunately, I didn't get a working source code with the Old API reaching your needs.
Although I've found a Microsoft.ClassicCompute provider, instead of the usual used Microsoft.Compute one, but still failing to have a working test.
I'm pretty sure you should no more "manually" use the old obsolete API, and should use modern Microsoft packages allowing management of Classic and "Normal" elements, like Virtual Machines, or Storage accounts.
The key package is Microsoft.Azure.Management.Compute.Fluent
You can find the documentation here: https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.management.compute.fluent?view=azure-dotnet
Let me know if you still need help.
I am in the situation that I need to access a ASP.NET Web Api that is using ADFS for authentication. I can hit it reliably through my browser by going through the ADFS login portal and getting the relevant FedAuth cookie. Unfortunately I need to access it from outside of a dedicated browser for use in a mobile app. The project is pretty much a slightly modified version of the standard visual studio web api template set up for Work and School Authentication (on-premises) and set up for cookie authentication.
bit of code from Startup.Auth.cs:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType
});
}
I can't seem to figure out where to start. I've tried requesting a access token from the ADFS and can get different versions of SAML assertions using relevant login info, but it gets rejected by the web API. Have I misunderstood how it's supposed to work?
From my understanding it's supposed to go like this:
How I think it's supposed to work
App requests a authentication token from the ADFS
ADFS gives the requestee an auth token if the information provided was correct
App makes request to the web API and sending the token along inside a cookie called FedAuth(by default anyway) as a base64 encoded string
Web Api sends the token to the ADFS to find out if the token is correct.
ADFS responds with some sort of success message
Web Api responds to the app either with a rejection or a piece of data depending on how authentication went.
This is what I have right now while trying to figure out how to get a hold of the correct tokens.
using System;
using System.IdentityModel.Protocols.WSTrust;
using System.IdentityModel.Tokens;
using System.Net;
using System.Net.Http;
using System.ServiceModel;
using System.ServiceModel.Security;
using Thinktecture.IdentityModel.Extensions;
using Thinktecture.IdentityModel.WSTrust;
namespace ConsoleApplication1
{
class Program
{
private const string UserName = "USERNAME";
private const string Password = "PASSWORD";
private const string Domain = "DOMAIN";
private const string ADFSEndpoint = "ADFS ENDPOINT";
private const string ApiBaseUri = "THE API";
private const string ApiEndPoint = "AN ENDPOINT";
static void Main(string[] args)
{
SecurityToken token = RequestSecurityToken(); // Obtain security token from ADFS.
CallApi(token); // Call api.
Console.ReadKey(); // Stop console from closing
}
private static SecurityToken RequestSecurityToken()
{
var trustChannelFactory =
new WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(new Uri(ADFSEndpoint)))
{
TrustVersion = TrustVersion.WSTrust13,
Credentials = { UserName = { UserName = UserName + "#" + Domain, Password = Password } },
};
var requestSecurityToken = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Bearer,
AppliesTo = new EndpointReference(ApiBaseUri)
};
RequestSecurityTokenResponse response;
var securityToken = trustChannelFactory.CreateChannel().Issue(requestSecurityToken, out response);
return securityToken;
}
private static async void CallApi(SecurityToken securityToken)
{
using (var handler = new HttpClientHandler { CookieContainer = new CookieContainer() })
{
using (var client = new HttpClient(handler))
{
handler.CookieContainer.MaxCookieSize = 8000; // Trying to make sure I can fit it in the cookie
var cookie = new Cookie {
Name = "FedAuth",
Value = Base64Encode(securityToken.ToTokenXmlString()),
HttpOnly = true,
Secure = true
};
handler.CookieContainer.Add(new Uri(ApiBaseUri), cookie);
var response = client.GetAsync(new Uri(ApiBaseUri + ApiEndPoint)).Result;
string result = await response.Content.ReadAsStringAsync();
Console.WriteLine(result);
}
}
}
public static string Base64Encode(string plainText)
{
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
return System.Convert.ToBase64String(plainTextBytes);
}
}
}
I can't quite remember what code I based my example of, but if anyone can point me in the right direction or tell me where I fucked up I'd appreciate it.
Edit: Sorry, forgot to add what I am getting.
The Web Api vomits out a bunch of debug information because an exception was thrown, telling me that a SecurityContextToken is expected instead of a saml:Assertion that I am apparently getting. Maybe my googlefoo is not powerful enough, but I can't seem to figure out where to start with this. Can I setup the api to accept SAML assertions or do I need to request the token in a different way?
You can't use WS-Fed to call a web API. You need OpenID Connect / OAuth as in Calling a web API in a web app using Azure AD and OpenID Connect.
It's for Azure AD but it does illustrate the flow.
What version of ADFS?
If 2.0, there is no OAuth support.
If 3.0, web API only - refer Securing a Web API with ADFS on WS2012 R2 Got Even Easier.
If 4.0, you have the full stack.