AzureAD / microsoft-authentication-library-for-dotnet

Microsoft Authentication Library (MSAL) for .NET
https://aka.ms/msal-net
MIT License
1.39k stars 340 forks source link

ConfidentialClientApplicationBuilder AcquireTokenForClient misses the grant_type=authorization_code #2311

Closed beniukdima closed 3 years ago

beniukdima commented 3 years ago

I am trying to use the Microsoft.Identity.Client library for OAuth2 grant authentication to http://businesscentral.dynamics.com/ to obtain the connection token from Azure AD. So I want to automate the steps described here for the Postman: https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-develop-connect-apps

Here is the code I am using: var confidentialClient = ConfidentialClientApplicationBuilder .Create(clientId) .WithClientSecret(clientSecret) .WithAuthority(new Uri(authorityUri)) .WithRedirectUri(redirectUri) .Build(); var scopes = new List { "https://api.businesscentral.dynamics.com/.default", "offline_access"}; var accessTokenRequest = confidentialClient.AcquireTokenForClient(scopes); accessToken = accessTokenRequest.ExecuteAsync().Result.AccessToken;

The problem is that the token is returned just fine but it is then can not be used to return the data from the v2.0 queries like that: https://api.businesscentral.dynamics.com/v2.0/BCinstance/api/v2.0/companies(guid)/items I've debug the source code of the Microsoft.Identity.Client package and found that the flow to get the token misses the step of getting the authorization code step which is a cause of that not "correct" token. I've also debug the flow when using the PublicClientApplicationBuilder: IPublicClientApplication app = PublicClientApplicationBuilder.Create("6c323996-83c7-4a52-aba9-01db32e131f9") .WithAuthority(new Uri(authorityUri)) .WithRedirectUri("http://localhost") .Build(); var token = app.AcquireTokenInteractive(scopes).ExecuteAsync().Result.AccessToken; And the token returned is totally fine, I also see the opening browser page where I need to confirm mine account and give a consent.

Will the flow of getting the OAuth2 token using the authorization code be inplemented in the ConfidentialClientApplicationBuilder? Should I use some other approach of getting that?

jmprieur commented 3 years ago

@beniukdima grant_type=authorization_code is implemented in AcquireTokenByAuthorizationCode Indeed AcquireTokenForClient is the client credential flow (for daemon application that request tokens on behalf of themselves)

If you are a building a web app (I suppose you are, since you want this flow on a confidential client app), please see Scenario: A web app that calls web APIs.

Note that if you are building a web app with ASP.NET Core, the recommended way is using Microsoft.Identity.Web

Also there are samples: https://docs.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code#web-applications

beniukdima commented 3 years ago

@jmprieur thx for the reply, I am looking on AcquireTokenByAuthorizationCode method, and have 2 questions: 1) How can I get the authorization code? I've implemented it like that, see the code below. 2) How this flow could be cached? So I do not make additional requests to get a new token by calling AcquireTokenByAuthorizationCode as this makes a new requests to get a new token per each call?

I've copied the DefaultOsBrowserWebUi class to mine app to be able to provide the consent var authResults = confidentialClient.GetAuthorizationRequestUrl(scopes).ExecuteAsync().Result; DefaultOsBrowserWebUi ui = new DefaultOsBrowserWebUi(); CancellationToken cts = new CancellationToken(); var authCodeResponse = ui.AcquireAuthorizationCodeAsync(authResults, new Uri("http://localhost"), cts).Result.AbsoluteUri;

Can this process be somehow automated like for the PublicClientApplicationBuilder, so I do not have to copy any code from the package code and invent some new "wheels" for getting that. Are there any plans to make it automated?

jmprieur commented 3 years ago

@beniukdima : which kind of application are you writing? I'm completely confused. What is your scenario: https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-flows-app-scenarios#application-scenarios. Is it a console app? a web app? do you use a framework? did you look at the links I provided above?

In ASP.NET or ASP.NET Core web apps, the code is obtained by ASP.NET (core). In public client applications, it's imbedded in AcquireTokenInteractive.

Once we understand the kind of app you want to build, we should be able to help you

beniukdima commented 3 years ago

We have a ASP.NET CMS website but it is not using any Azure AD authentication for the frontend/backend users. The feature we are working now is to be able to execute the OData requests from the MS cloud D365BC https://businesscentral.dynamics.com/ but using the OAuth2.0 Azure AD authentication that uses the flow described here: https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-develop-connect-apps So we need some mechanism of automating that process: so once at least some Azure AD person granted the permissions his authorization code and token and refresh token are stored in the cache so they could be then reused when the D365BC requests are made after some time/days are passed, so the user should not see the login screen again.

So from the scenarios you posted on the link above it seems it suits the Web App -> We Api flow using the Authorization, but with some caching/auto retrieval of the current token Is it more clear now?

So it looks like we need to use: get the authorization code first, and then use confidentialClient.AcquireTokenByAuthorizationCodeAsync But how we can check the current token is still valid so we do no need to ask for authorization code again? Automatically get a new token once the old token is expired using some cached "refresh token"? So our goal is to show the confirmation consent page to the user only one time to conifrm the authorization and then to be able to use the cached token/refresh token silently

jmprieur commented 3 years ago

@beniukdima : the code can be redeemed only once. However, When you redeem it with MSAL, the access token gets in the cache with the refresh token. You can then use AcquireTokenSilent to get another token (for the same or different scopes/resources)

To get the authorization code, you could let ASP.NET do it (See ASP.NET authentication), or ask MSAL.NET for the URL to get the code (https://docs.microsoft.com/en-us/dotnet/api/microsoft.identity.client.iconfidentialclientapplication.getauthorizationrequesturl?view=azure-dotnet), and process it yourself.

beniukdima commented 3 years ago

@jmprieur thx for the help I've already followed that way. I have an issue regarding caching/restoring the cache state. The web site is hosted in IIS and once the ConfidetialClientBuilder has got the authorization code by vising the url from GetAuthorizationRequestUrl method, then using that it requests the token by calling AcquireTokenByAuthorizationCode then once the token is expired the call to AcquireTokenSilent is made. Once the application pool is restarted/website is restarted in IIS the static instance of ConfidetialClientBuilder is erased and then re-initialized, so it then asks for the confirmation at obtaining the autorization code again and repeats the flow. Is it possible somehow to export/store the internal tokens cache in file/database so it can be restored back once the app is restarted so it will not ask for the authorization code again?

jmprieur commented 3 years ago

Sure @beniukdima : you need to add token cache serialization. Microsoft.Identity.Web can help you (even .NET FW): Microsoft.Identity.Web support for ASP.NET classic

bmukes commented 3 years ago

In reading your last comment you state. the static instance of ConfidetialClientBuilder is erased. have you discovered a way to use a non static instance of ConfidentialClientBuilder. All of the Microsoft samples show using a non static instance and I have been unable to get a non static instance to work. I can get a static instance to work unless the user logging in is from a Guest account. I think I will adopt your idea and debug the code since I have it pulled down on my PC.