Calling Azure AD secured web API with client / secret - c#

I've been at this whole day and it's driving me NUTS. I've got a web API published on Azure which is secured with Azure AD authentication. I've been using this API in combination with a mobile app for quite a while now. On the mobile app, I use the library for client authentication (Microsoft account which is in AD) and this works perfectly. Within the app, API requests are authenticated with the X-ZUMO-AUTH header.
Now for a new part of the application, I've got a C# console app which also needs to be able to call the app. This needs to run stand-alone, so without asking for user credentials. So I created an application in Azure AD, gave it permissions to the Web API instance and got me some code to get an authentication token. That token is then passed into the HttpClient object:
AuthenticationContext ac = new AuthenticationContext(authority);
ClientCredential clientCred = new ClientCredential(clientID, clientSecret);
AuthenticationResult authenticationResult = await ac.AcquireTokenAsync(resource, clientCred);
string authToken = authenticationResult.AccessToken;
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, endpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
HttpResponseMessage response = await client.SendAsync(request);
Server auth is set-up like this:
string aadTenant = configProvider.GetConfigurationSettingValue("ida.Tenant");
string aadAudience = configProvider.GetConfigurationSettingValue("ida.Audience");
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = aadTenant,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = aadAudience
},
});
I have triple checked all the variables. I also verified that the Bearer token is correctly passed to the service (Fiddler shows it). Everything seems absolutely fine, but still, the server keeps responding with Unauthorized.
My hunch is that this has something to do with the Web API. It's been set up as mobile services back-end. I'm suspecting that this is somehow interfering with 'normal' authentication schemes and the 401 is coming from mobile services not understanding this authentication request, even though the bearer authentication has been setup. But this is just a hunch and I'm not sure whether it actually makes sense or not.
I've seen the related questions on this topic, and all of the samples (used this one as the primary source of inspiration). I've tried all of the variations out there, to no avail. So what I'm looking for is ways to find out where the actual problem is. "Unauthorized" is a bit vague to say the least, so I'm trying to find out why the calls are returned with this response. Any help is greatly appreciated.
Additional endpoint info
As requested, here's some info on the endpoints I'm using:
authority = https://login.microsoftonline.com/{aad-tenant-name}
Also tried the OAuth2 token endpoints listed on the management page:
authority = https://login.microsoftonline.com/{aad-guid}/oauth2/token
authority = https://login.microsoftonline.com/{aad-guid}/oauth2/authorize
For the client, information I'm passing in the ClientID and generated secret I got from the AAD Application page for the application I created. I have granted the application permissions to access the App Service instance for my web API endpoint. Those calls are going to:
https://{app service url}.azurewebsites.net/api/controller
Whilst trying to get this going I noticed more weird behavior. I thought for now I'd just set this particular controller to [AllowAnonymous] so that I could call it without any authentication. But when I do so and omit passing in the Bearer token, I still get Unauthorized as response. That kind of enforces my idea that there's something wrong on the server side and that this might have something to do with how the mobile app stuff is strapped to the webapi controllers.
Never mind, the controller being forced to authorize was caused by Web App level authentication settings, documented here

Related

How to authenticate an HttpClient connection with an external provider (Google)

Edit:
Here is my question reformulated:
I have a web server with secured api endpoints - one must have been authenticated with Google prior to using them. I implemented Challenge and Callback endpoints for that.
This works well from a browser with my SPA web front-end. The user gets redirected to the Google website to sign-in and then gets redirected back to my webapp; the browser then has the authenticated cookies and the webapp can use the endpoints to update its state.
I also have a WPF application that will communicate with the web server.
I want the WPF application to do the same as the web front-end: Use the web api endpoints after being authenticated with Google. The connection between the WPF application and my web server is done through an HttpClient.
My problem is I don't know how to authenticate that HttpClient connection between the WPF app and the web server.
I tried using the same Challenge endpoint but the response I get is of course the HTML from the Google Sign-In page, so I guess I can't use that with an HttpClient...
I also tried authenticating with GoogleApis from the WPF app and use the authenticated token to set cookies in the HttpClient but apparently this is not compatible.
How to authenticate an HttpClient connection to a web api with an external provider such as Google?
Original question:
From a WPF application, the user authenticates with Google with this code:
using Google.Apis.Auth.OAuth2;
...
public void Authenticate()
{
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = "myClientId",
ClientSecret = "myClientSecret"
},
new[] { "email", "openid" },
"user",
CancellationToken.None).Result;
}
This works and the UserCredential object contains the authenticated token:
How to embed this token information in a web request made with an HttpClient in order to call my webapi endpoint?
I think the request must include some cookies to inform the server that it has been authenticated, but I don't know which ones exactly.
The endpoint on the server-side validates that the user is authenticated with the help of IdentityServer:
var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
If I got your question right, you just have to set the Authorization header
var credentials = await GoogleWebAuthorizationBroker.AuthorizeAsync(
clientSecrets,
new[] { "email", "openid" },
"user",
CancellationToken.None);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
credentials.Token.TokenType,
credentials.Token.IdToken);
Maybe you'll find below a helpful hint to better understand OpenID :)
The confusion stems from mixing GoogleApis and IdentityServer frameworks.
Authentication/authorization can be achieved using either of them.
Objects from Google.Apis.Auth.OAuth2 and IdentityServer4 namespaces are not designed to interact.
No manual cookie handling is necessary, for sure.
Ask yourself to whom does Google provide trust for the user. If it calls back to WPF, then webapi trusting WPF is a separate issue.
You answer your own question in the question:
the browser then has the authenticated cookies and the webapp can use
the endpoints to update its state
HttpClient needs to send those same cookies.
How do I set a cookie on HttpClient's HttpRequestMessage
If I understood your question right, then I faced the same problem not too long ago.
The way I implemented it is that in the backend, no matter who tries to access the endpoint, they had to send a Bearer X authorization token.
The token contained the identity of the client that wanted to access the resource, and I checked if he was permitted.
No matter what kind of client wants to access the endpoint, it just has to have that authroziation header in the request that he sends and the backend will treat it the same.
In my scenario, I used an authentication service that returns a cookie to the client with a certain JWT that contains the identity information.
Then from the client I send in every request the JWT received from the authentication service as an authorization header to the backend.
The reason I had to put the JWT that I receive from the service in a header, is that the authentication service and the backend service are not in the same domain, so cookies cant be shared.
This results in such design that no matter how you authenticate the client, the end result must be some sort of token that the backend can receive and read.
Hope this helps.

Azure AD authentication token fails web api authorization

I have a web-api on Azure that requires authorization and I am using Azure AD to authenticate accounts and generate access tokens.
I can successfully acquire access tokens from Azure AD with ADAL for the same account in two different ways, but only one of them is authorized by the web-api, the other one fails.
The following is authenticating an account interactively and the token is authorized by the web-api
result = AuthenticationContext.AcquireTokenAsync(resource, clientId, redirectUri, new PlatformParameters(PromptBehavior)).Result;
where resource is web-api application id (guid).
The following is authenticating an account non-interactively with a given user name and password, but the token is not authorized by the web api
UserPasswordCredential cred = new UserPasswordCredential(userName, password);
result = AuthenticationContext.AcquireTokenAsync(resource, clientId, cred).Result;
where resource = https://{tenant}/{api name}.
The web-api call is as follows:
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await httpClient.GetAsync(ApplicationCallUri);
Both ways return identical AuthenticationResult objects (apart from tokens and time stamps) and I cannot see why authorization fails for the second one.
The web-api response is "Authorization has been denied for this request."
Since authentication succeeds for both ways, I assume it must be something with at the web-api's side. Help is much appreciated. Thanks.
Thanks to juunas who pointed out the audience parameter I realized that the web api was set to expect tokens for only one the two audience values. I added a second option for bearer authentication and it works for both scenarios.
Thank you juunas!

Dynamics CRM api HttpClient Request Authentication with ADFS 3.0

I have an on-premise Dynamics CRM (2016) that is configured with ADFS (3.0). When a user want's to Login, they get redirected to the ADFS login page and the user enter their Windows AD credentials.
From a .net core application I need to make request to the CRM api using HttpClient. When I try to send the credentials like I normally would for a Windows Auth CRM it doesnt work. I get a 401 Unauthorized. Like below.
HttpClient client = new HttpClient(new HttpClientHandler() { Credentials = new NetworkCredential("myuser", "mypassword", "mydomain") });
var result = client.GetAsync("https://mycrmaddress/api/data/v8.0/accounts");
I also tried using Adal to retrieve a token and attach it as a bearer token to the request but I'm unable to get a token with adal. When I try I receive the following:
The authorization server does not support the requested 'grant_type'. The authorization server only supports 'authorization_code'
ADFS 3.0 doesn't support this flow.
I cannot upgrade to ADFS 4.0 so I would like to know what are my options to make an authenticated call to CRM api (without prompting a login window as this application is a service).
Is there any configuration I can do on ADFS so my first example work? Or is it possible to do it with Adal even if it's ADFS 3.0? Or any other solution...
I found the answer to my question. It's kinda hackish, but I tested it myself and it works. As a temporary solution this will do the trick.
Details are available here: https://community.dynamics.com/crm/f/117/t/255985
ADFS 3.0 supports the Authorization Code flow and this what we will use in this case.
We need to retrieve an authorization code. Normally at this steps a windows is prompted to the user to enter its credentials. By doing a POST and sending the user/password it's possible to retrieve an authorization code.
{authProvider} - ADFS Uri - something like
https://adfs.mycompany.com/adfs/oauth2/
{ClientId} - The Guid used to
by your infrastructure team to add your application to ADFS
{RedirectUri} - The IFD Uri for dynamics - should match the redirect
Url used to by your infrastructure team to add your application to
ADFS
username - The User set up on ADFS and in Dynamics
password - The password for the above user
Then we make the following call with these information using HttpClient.
var uri = $"{authProvider}authorize?response_type=code&client_id={clientId}&resource={redirectUri}&redirect_uri={redirectUri}";
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string,string>("username",username),
new KeyValuePair<string,string>("password",password),
});
var responseResult = _httpManager.PostAsync(uri, content).Result;
The response content will be an html page (Remember normally this flow prompts a login page to the user). In this page there will be a form that contains the authorization code. using a library like HtmlAgilityPack retrieve the token. This is the hackish part of the solution.
Now that we have an authorization code we need to retrieve an access token.
For that we need to make the following call
var uri = $"{authProvider}token";
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string,string>("grant_type","authorization_code"),
new KeyValuePair<string,string>("client_id",clientId),
new KeyValuePair<string,string>("redirect_uri",redirectUri),
new KeyValuePair<string,string>("code",code)
});
var response = await _httpManager.PostAsync(uri, content);
The response content will be a json string that will contain the access token.
With the access token, make the call to CRM rest API.
You will need to attach the token to the HttpClient in the header as a bearer token.
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",token);
httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
From now on you can make calls to CRM api and you will be authorized. However be carefull normally access token are short lived. You will either need to increase their lifetime or request a new token everytime it's expired.

Sharepoint REST api and MVC AAD connect

My need is to execute this query https://<tenant>.sharepoint.com/_api/search/query?querytext=%27contenttype:articles%27 thru Sharepoint REST api from server side in C#.
I have Oauth2 connection from the MVC portal, so my goal is to retrieve token from connection and send it as bearer token to sharepoint endpoint.
I mean something like that
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new NaiveSessionCache(userObjectID));
ClientCredential credential = new ClientCredential(clientId, appKey);
AuthenticationResult result = await authContext.AcquireTokenSilentAsync("https://<tenant>.sharepoint.com/", credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://<tenant>.sharepoint.com/_api/search/query?querytext=%27contenttype:articles%27");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
but obviously, I can not retrieve the token...
On another side, I have built an app with ADv2 and GraphServiceClient that is working well, but I don't know how to translate the query in graph model (and I don't have any admin-consent).
So, I have 2 ways to resolve my issue, I'll like better use the 2nd option with microsoft graph api, but any help is welcome.
Thank you.
Around Search
The graph search API has limited capabilities, first it will only search in the current site collection (drive) you're targeting, second I'm not sure at the moment it would support a search by content type (maybe with a $filter...)
But it could be an (easier) option if that fits your constraints.
Around auth & auth
In both cases (graph or SharePoint search), what happens when people get to your application (asp.net MVC) is that the authentication middleware takes care of redirecting the user to AAD, get an access token to your app, redirects it to your app which uses that access token to create a session on the app.
My point being: at this point, all you have are:
An access token to your app (not the graph, not SharePoint
A session against your app
You need to do a couple of things to get to SharePoint/the graph:
Intercept and keep the token server side (add it to the session?) if that's not already being done by your implementation of the middlewares
Use that access token + you app id/secret/certificate to get an access token to SharePoint/the graph against AAD
Make sure your application has permissions in AAD to talk to SharePoint/The proper graph API's
Here is a sample on how to get from "I have the access token to my app/api" to "I have an access token to the graph/SharePoint" using MSAL.
Note: I'm using a certificate here, but you could be using a secret instead
var cac = new ClientAssertionCertificate(ApplicationId, CertificateProvider.AppCertificate);
var ua = new UserAssertion(apiAccessToken);
authenticationResult = await authContext.AcquireTokenAsync(resource, cac, ua);
I'm not providing the code on how to intercept the token/get it here because your question is unclear on your current authentication and authorization configuration as well as what MVC "flavor" are you using (asp.net core + middlewares, classic + owin, something else?). I suggest you start another question with more details on that specific point.

MSAL + Azure App Services

I've posted this before, but the thread became pretty extensive and confusing and a resolution was never met. I'm reposting with a clear and concise block of code and my desired outcome.
I'm looking to use client-flow authentication for an Azure App Services backend.
I'd like to use MSAL, to support both Microsoft Accounts (MSA) and AAD accounts. Been stuck on this for weeks with no resolution in sight.
PublicClientApplication myApp = new PublicClientApplication("registered-app-id-in-apps.dev-portal");
string[] scopes = new string[] { "User.Read" };
AuthenticationResult authenticationResult = await myApp.AcquireTokenAsync(scopes);
JObject payload = new JObject();
payload["access_token"] = authenticationResult.AccessToken;
payload["id_token"] = authenticationResult.IdToken;
user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory, payload);
Why doesn't this work?
What do I have to do to get it to work?
Getting a 401 exception, tried with MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory as well as MobileServiceAuthenticationProvider.Microsoftaccount
--App Service Auth Config for Microsoft Account:
ClientID and ClientSecret as it appears in apps.dev.microsoft.com
--App Service Auth Config for AAD:
ClientID as it appears in apps.dev.microsoft.com
Issuer URL: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
Client Secret: (Blank)
Been having the same issue, having set up Azure Active Directory authentication on the App Service & attempting to authenticate from a WinForms client using MSAL. Turns out that, as of the time of this writing, Azure App Service does not support AAD V2 (including MSAL). Found the below note here:
At this time, AAD V2 (including MSAL) is not supported for Azure App Services and Azure Functions. Please check back for updates.
So ADAL seems to be the only viable option at the moment, unless you handle the authentication inside your backend code yourself.

Categories

Resources