Some Background Information
I am building a game in Unity, using C#. Since I am using Firebase and the ready-made Unity Firebase SDK won't work in my case1, I have resorted to interfacing with Firebase's REST API through C#'s HttpClient class (which comes with System.Net.Http).
I am currently struggling with the Firebase Authentication APIs. For those unfamiliar with OAuth APIs, when I call the Sign Up or Sign In endpoints, I get both an ID Token and a Refresh Token. ID Tokens expire; refresh tokens do not. When the ID Token expires, I must call another endpoint, called the Token Exchange, to get a new ID token using my refresh token.
1 There is a rumored Google Identity Toolkit C# library, but the first result returned by that search I found it by leads to a 404 error instead of documentation. š¢
What Does Work
Following the Firebase guides and the underlying Identity Toolkit guides, I have been successful so far interfacing the token exchange endpoint with a cURL command from bash:
curl 'https://securetoken.googleapis.com/v1/token?key=[MY FIREBASE API KEY]' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=refresh_token&refresh_token=[MY REFRESH TOKEN]'
of course, I replace [MY FIREBASE API KEY] with my Firebase Web API key, and [MY REFRESH TOKEN] with the refresh token returned from the sign in/sign up endpoints.
However, despite my many attempts, I have not been able to replicate this cURL command in C#!
My Failed Attempts
1.
Here's my original code that didn't work.
public async Task<bool> ExchangeToken()
{
FormUrlEncodedContent body = new FormUrlEncodedContent(new Dictionary<string, string>() {
{"grant_type", "refresh_token" },
{"refresh_token", Uri.EscapeDataString(user.refreshToken) }
});
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "https://securetoken.googleapis.com/v1/token?key=" + apiKey);
message.Content = body;
message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
HttpResponseMessage res = await client.SendAsync(message);
}
Unfortunately, I get this 401 (Unauthorized) error response:
{
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
}
This is quite strange, considering that I get a 2XX (OK) response from what should be an equivalent cURL command...
2.
Thanks to a very nice website I just discovered, I was able to "convert" my cURL command into C# code. However, this did not work. I got the exact same error as attempt #1.
public async Task<bool> ExchangeToken()
{
using (var request = new HttpRequestMessage(new HttpMethod("POST"), "https://securetoken.googleapis.com/v1/token?key=[I WOULD INSERT MY KEY HERE]"))
{
request.Content = new StringContent("grant_type=refresh_token&refresh_token=[INSERT REFRESH TOKEN HERE]", Encoding.UTF8, "application/x-www-form-urlencoded");
var res = await client.SendAsync(request);
}
}
Possible Leads
All of my other requests to all of the other endpoints work. There are two notable differences: 1) the API is technically not Firebase's, but Google Identity Toolkit's. 2) This is the only endpoint that I'm using that uses a Content-Type header of application/x-www-form-urlencoded instead of application/json.
My Question / TL;DR
How do I interface with the Google Identity Toolkit API's Token Exchange endpoint using C#?
(Though I'm currently using the HttpClient class, I'm totally open to other solutions! They just have to be compatible with Unity3D.)
Thanks in advance!
Google wants you to be using Firebase for auth. While Firebase does not have a .NET client SDK shipped by Google, there are other options.
If you don't use Firebase and perform the requisite tasks with oAuth/OpenID Connect endpoints yourself, that works too. There is a Google .NET SDK that helps you with this...a little bit . The SDK lists Google APIs that are "supported" by the SDK; Google Cloud Identity v1 and v1beta APIs are both on the list. So you could call Google Cloud Identity endpoints via this .NET SDK. You do have to understand the implications of various auth flows whereas in Firebase much of that machinery is abstracted away from you as the app developer.
After playing around with the code a little more, I remembered that I was setting default authorization on my HttpClient:
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", idToken);
This was included for all of the other API calls, as they require authentication with the ID Token. However, this seemed to be confusing the token exchange endpoint; it must 1) look for an authorization header, then 2) use the refresh token supplied in the message contents.
How to fix this issue
Instead of setting the default header on your HttpClient (Which is supposed to be reused for all of your HTTP requests), do the following:
var req = new HttpRequestMessage(/* HttpMethod enum value, endpoint URL/IP */);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", user.idToken); //Set the authorization per-request, not globally
var res = await client.SendAsync(req);
Then just simply call the Token Exchange endpoint!
From the Firebase REST API Documentation:
You can refresh a Firebase ID token by issuing an HTTP POST request to the securetoken.googleapis.com endpoint.
https://securetoken.googleapis.com/v1/token?key=[API_KEY]
Just provide a refresh_token parameter in the body (along with the mandatory grant_type with value refresh_token). DO NOT provide an authorization header.
Hope this helps anyone with the same problem!
As an alternative, you can also use https://github.com/google/apis-client-generator to generate the c# client from the discovery docs. I had some naming conflicts when I generated mine, but after resolving those it works.
Related
I have a minimal API .NET 7 installed on an external web server and use JWT for authentication. For testing I created a few endpoints (with authentication and also without) so I can test the web API via Postman. I start Postman from my private machine and access the web address of the API to test everything.
Now everything works as expected. I can log in via Postman, then I get JWT and if I enter JWT in Postman, then I can also access protected endpoint and get the data from the Web API.
Now I have created a desktop application in MAUI .NET 7 and I want to use this web API. Also here the access to unprotected endpoint works as well as logging in with receiving the JWT. Only the last part of the whole thing does not work anymore and that is access to a protected endpoint with the delivery of JWT for which I constantly get the message 401 Unauthorized. If I then put the same JWT into Postman, then the request goes through Posstman and I get the data from Web API!
I have been looking for a solution and have tried all possible code examples from the internet. For example:
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("http://api.mywebsite.com:64591/secret")
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token.token);
var response = await _httpClient.SendAsync(requestMessage);
or
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", Token.token);
var RawData = await _httpClient.GetStringAsync("http://api.mywebsite.com:64591/secret2");
In some places I read that there were problems with the change to .NET 6. The solution was new NuGet packages, but since I'm already on .NET 7, I installed the latest versions.
There was also a post suggesting that in the web API you set issuer and audience to false. I did that as well, but to no success.
ValidateIssuer = false,
ValidateAudience = false,
Does anyone have a working code for MAUI native app that consumes minimal API?
EDIT
Following Heretic Monkey's suggestion, I installed Wireshark software and analyzed Network Transfer.
Here is what I found:
the token I receive from Web Api after authentication, I also send in the same form when requesting to an Authorized Endpoint (so received and sent token from client are identical). My conclusion here is that the token is correct.
When the request with the JWT is sent from the client to the server (Web Api), I then get the error message 401. In the log I see below information with the reason Bearer error="invalid_token":
Hypertext Transfer Protocol
HTTP/1.1 401 Unauthorized\r\n
[Expert Info (Chat/Sequence): HTTP/1.1 401 Unauthorized\r\n]
[HTTP/1.1 401 Unauthorized\r\n]
[Severity level: Chat]
[Group: Sequence]
Response Version: HTTP/1.1
Status Code: 401
[Status Code Description: Unauthorized]
Response Phrase: Unauthorized
Transfer Encoding: chunked\r\n
Server: Microsoft-IIS/10.0\r\n
WWW-Authenticate: Bearer error="invalid_token"\r\n
X-Powered-By: ASP.NET\r\n
Date: Sun, 18 Dec 2022 09:07:00 GMT\r\n
\r\n
[HTTP response 1/1]
[Time since request: 0.047969000 seconds]
[Request in frame: 790]
[Request URI: http://api.myserver.com:64591/secret2]
HTTP chunked response
End of chunked encoding
Chunk size: 0 octets
\r\n
File Data: 0 bytes
There are only two error reasons I could think of:
I still have a bug in my minimal API (Web Api) and that is regarding the JWT I get from the client and somehow still need to convert/crimp the JWT maybe!? By the fact that I may use JWT in exactly the form that is sent to client, then it may be that it is wrong and that is why this error message "invalid_token" comes.
the second cause could be NET 7, so an error that occurs not because my code is wrong but because it is implemented incorrectly in NET 7 (is of course not probable but not impossible).
Maybe someone has a suggestion how I can fix this error?
If this doesn't work (i.e. a request to Web Api with JWT authentication), then Web Api is unusable in NET 7 and I really can't imagine that.
So I truly assume that the bug is in my implementation (either server/minimal Api or client MAUI NET 7).
Thanks
The problem was already kind of strange, because even the many tutorials and posts in the form as they are given in Internet will not work. But if you copy generated token out (e.g. from debug mode) and use it in Postman, then everything will work nicely and this is something that confuses you a lot. Fortunately, there are still people who have incredible mind and can detect such inconsistencies. I wouldn't have seen this in 1000 years either :)
See: https://learn.microsoft.com/en-us/answers/questions/1133200/401-unauthorized-consuming-web-api-with-jwt-authen.html
In my case I noticed that
response.Content.ReadAsStringAsync().Result
in .NET MAUI will return "+token+", I have trimmed the quotation mark (") , and it worked with me
using HttpResponseMessage response = await client.PostAsJsonAsync("Login", loginData);
response.EnsureSuccessStatusCode();
string token = response.Content.ReadAsStringAsync().Result;
UPDATE
I was able to get a working request posted. The third-party API has us sending the Token (which is basically a Guid) as a bearer token. Azure appears to do some sort of pre-validation on this. When I swapped out the GUID with a true randomly generated bearer token, it worked.
I do still wonder if there's a way to disable this check-in Azure. The "bad" Bearer token works for GET requests but fails for POST/PUT requests.
Summary of the Application
We have Azure Functions (i.e., Time Trigger, Orchestrator, Activities) that look for items in an on-prem queue table in SQL and then POST it to a third-party API via JSON.
The third-party API requires an Authorization header with the POST request.
Technical Overview
dotnet core 3.1
azure function runtime ~3
Additional Information
This codebase worked fine during UAT back in April-May of this year. It then sat idle until we rebooted the project a couple of weeks ago.
Outbound requests are not proxied through APIM. They're sent directly to the third-party API
Application Insights is configured for the Azure Function
What works
All of the GET requests. No issues at all.
What doesn't work
POST requests. I proxied the requests to a beeceptor to see exactly what was being received. When the Authorization header is included most of the headers are stripped (I.e., Content-Type, Content-Length) and the Body of the request is blank.
If I removed the Authorization header then all headers and body are received as expected.
Question
I can only assume at this point that some Azure service, pre-flight check, security policy is intercepting the Authorization header thinking it's intended for "itself", but I have absolutely no idea what it could be. I've been on Google now for days.
Simplified Version of Code
using var client = new HttpClient();
client.DefaultRequestHeaders.Clear();
// Request params are dynamic and a helper method builds the full request path
var path = PathBuilder(queueItem.RequestParams, queueItem.Request.UrlPath);
// This can change in code not shown if the request is sending files
var contentType = "application/json";
client.BaseAddress = new Uri(queueItem.Request.Client.BaseApiUrl);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { NoCache = true };
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", queueItem.Request.Client.AuthToken);
// queueItem.Data is JSON
HttpContent json = new StringContent(queueItem.Data, Encoding.UTF8, contentType);
return await client.PostAsync(path, json);
Also...
I've confirmed the JSON body is valid
The code did work and has remain unchanged
Given all that youāve tried, it might be a long shot, but have you tried to add the token like:
client.DefaultRequestHeaders.TryAddWithoutValidation(āAuthorizationā, ābearer token hereā¦ā);
and then check whether the try succeeded or not?
I have a Postman request sent by a partner/client that requires Oauth2 to hit their web service endpoint. Here is the authorization section:
I need to re-create this request in C#. Should be easy, just click on Code link, and grab the sample code in C# - RestSharp. Two problems:
1- When I execute this in Postman, I get The SAML2 token is not valid because its validity period has ended. So then I click the Get New Access Token button, and I get challenged for credentials:
(With the Client ID and Client Secret, shouldn't I be able to bypass this challenge?)
2- Ultimately I need to be able to run this request in my c# app. When I click the Code link in Postman it gives a nice C# example, but the problem with the sample code generated by Postman is that it assumes the bearer token has already been obtained, and just plops that into the source sample. But the obtaining of the bearer token is a very important piece that Postman omits. Here is my specific example, copied from Postman:
var client = new RestClient("https://myurl-here:7148/foo/ODataV4/WS3_stuff?Company='abc123'");
client.Timeout = -1;
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Authorization", "Bearer ey...massive-string...Zb");
request.AddHeader("Cookie", "ApplicationGatewayAffinity=ab721.more.d6c1a341bc; ApplicationGatewayAffinityCORS=ab..more...bc");
request.AddParameter("application/json", "{a-bunch-of-json-here}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
What are the pieces I'm missing? How can I get the C# equivalent of whatever Postman is doing to obtain the bearer and those Cookie values? And I thought there's a way for applications to achieve all the Oauth handshaking without sending the user to that MS window as in screenshot above - but how is it done?
ClientID and ClientSecret are there to identify your application not the user. They are sent to the Authorization Server so that the server knows that it can issue access tokens to this application. User authentication is another thing. You will always have to authenticate the user in order to get an access token which allows to access that user's data.
Unless you only need to authenticate your application and want to access data which does not belong to any user. In that case you need a client credentials flow, an OAuth flow which allows your application to get an access token.
Postman is a tool for making calls to APIs, it just generates the code which enables you to make the API call. Have a look at some C# OAuth clients (or maybe something for your framework, if you're using one). Those clients will enable you to easily generate new access tokens. Still, if you need a user's access token remember that you will need your users to open a browser. If you're developing a backend app, which does not serve any pages then you can have a look at the OAuth device flow, which enables you to authenticate users on a different device than your app runs.
As for the cookies - make sure whether you really need them. If you're calling an API chances are that those cookies are not required to make the request.
I'm working through the JWT impersonation flows documented here and here. I'm using C#, and though I have worked through a few of the quick start applications, I'm still having some issues.
Existing Flow
The flow I have so far, which seems to be functional in DS sandbox/dev/demo, is:
Send user to DocuSign (oauth/auth). scope is "signature impersonation". (I've tried it with a bunch more permissions thrown in as well.)
After DS auth and impersonation grant, user shows back up on my web app with an authorization code
Take that authorization code and post it to oauth/token to get an access token for my target user
Take that access token and call oauth/userinfo to get the target user's IDs and URL
Create a JWT, sign using shared key pair between my web app and DS, and post it to oauth/token. Receive a 200 response with a seemingly-good-looking token.
This all seems to work correctly so far: all DS calls come back with 200s and data which is shaped as I expect.
Problems
The issue is that I can't actually successfully use that token from the final step to perform further action as the user who my app is impersonating. (I am being sure to use the base_url for the associated user.) When I request a GET from the suggested endpoint (brands), I receive this back:
{
"errorCode": "AUTHORIZATION_INVALID_TOKEN",
"message": "The access token provided is expired, revoked or malformed. Authentication for System Application failed."
}
The response which provided the authorization token includes an expires_in value in the thousands of seconds, and I'm performing all of these requests in serial in my web application. So, expiration or revocation should not be possible at this point. I also haven't touched the token at all, so I would expect it to be well formed.
Here's the code I'm using to post to that endpoint, if it's useful:
private async Task<IgnoreMe> GetBrands(UserInfoAccount account, AccessTokenResponse accessToken)
{
var client = _clientFactory.CreateClient("docusign");
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri($"{account.BaseUri}/restapi/v2.1/accounts/{account.Id}/brands"),
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken!);
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
return IgnoreMe.Fail;
}
return IgnoreMe.Succeed;
}
The args to this method are the values which came back from previous API calls: the AccessTokenResponse is from the impersonation call.
I've also tried sending similar requests to several other top-level user/account endpoints, and have received the same or similar errors for all of them.
What am I missing here?
Your flow is a mix if Auth Code Grant and JWT. You are using both.
The token from step 3 should work (But you can omit "impersonation" as it's not required for Auth Code Grant).
The token expires after 8 hours. That may be the reason for your error. You'll need to obtain a new one.
In this particular case, the problem was that I had used the wrong ID for the sub value when constructing the JWT.
Results from the oauth/userinfo endpoint I'm using come back structured as a top-level user ID which is associated with a bucket of accounts. I had used an account ID from one of those buckets rather than the top-level user ID.
I am trying to use Google Cloud Vision via a rest http request using c#. As described here, I tried to authenticate with the api key as a parameter:
string uri = "https://vision.googleapis.com/v1/images:annotate?key=" + API_KEY;
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
[...]
response = await client.SendAsync(request);
However, I always get a 403 PERMISSION DENIED Code:
Cloud Vision API has not been used in project XXXXX before or it is
disabled. Enable it by visiting
https://console.developers.google.com/apis/api/vision.googleapis.com/overview?project=XXXXXXX
then retry. If you enabled this API recently, wait a few minutes for
the action to propagate to our systems and retry.
Of course I checked, The API is activated, enabled and there are no API restrictions:
Since there seemed to exist some problems with that authentication method, especially with cloud vision, I tried authentication via an access token of a service account that I created. I gave the service account full access just to be sure that there is no issue with the rights of the service account:
string uri = "https://vision.googleapis.com/v1/images:annotate";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
request.Headers.Add("Authorization", "Bearer " + ACCESS_TOKEN);
response = await client.SendAsync(request);
Still, same error message. Same goes with curl:
curl -k -X POST -H "Content-Type:application/json"
-d "#{REQUEST_AS_JSON_OR_PATH_TO_JSON-FILE}"
https://vision.googleapis.com/v1/images:annotate?key={API_KEY}
What am I missing out?
Turned out it is NOT sufficient to activate an API key (even when its without any restrictions), but one needs to actively go to the Google console menu -> APIs and Services -> Library -> Search for "Cloud Vision" -> klick on the Cloud vision API and activate it. It takes a couple of minutes. The status of this is not shown in your dashboard.
Then, you can simply authenticate with the api key as URL parameter.