Blazor WASM B2C - Redirect on AccessTokenNotAvailableException - azure-ad-b2c

I am working on a Blazor WASM application that uses B2C to authenticate the users and am having trouble getting the access token to refresh when I set the login mode to 'Redirect' rather than 'Popup'. I've pulled back my code to the absolute minimum which is essentially the tutorial at: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-azure-active-directory-b2c?view=aspnetcore-5.0
As per Microsoft's examples, I wrap my http calls in a try/catch block as below with the intention that if the Access token expires, a new access token is retrieved and the page reloaded:
try
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
For purpose of testing, I have set my Access Token timeout to 10 minutes and note that when this expires (and I had used the default Login mode of 'Popup'), a new Access token is obtained and the current page is returned to.
If however I change the LoginMode to 'Redirect' rather than 'Popup' and repeat the test, I receive the following error message within the address bar:
...authentication/login-failed?message=State%20mismatch%20error.%20Please%20check%20your%20network.%20Continued%20requests%20may%20cause%20cache%20overflow.
Interestingly though, it does appear to have updated the Access token as a quick flick between pages and I am able to retrieve the data.
Has anyone else experienced this or have any thoughts how to resolve this?

Related

.Net Core 3.1 Azure Web App - Failed to acquire token silently as no token was found in the cache. Call method AcquireToken

I have an Azure Web App that authenticates a user which then navigates to a page where some Sharepoint documents are retrieved and displayed in the app.
Most of the time the application works fine, but ocassionally App Insights will highlight that Failed to acquire token silently as no token was found in the cache. Call method AcquireToken. Some users report issues from time to time on this page (it's inconsistent so it might happen a few times a day with a somewhat large user base). The problem is that currently the error isn't handled and I'm trying to figure out how to make the call to AcquireTokenAsync.
The following is the method that returns the token (or doesnt):
private async Task<string> GetUserAccessToken()
{
try
{
// Credentials for app
// _clientId and _clientSecret represent the app info - not shown here in code
ClientCredential credential = new ClientCredential(_clientId, _clientSecret);
//Construct token cache
ITokenCacheFactory cacheFactory = Request.HttpContext.RequestServices.GetRequiredService<ITokenCacheFactory>();
TokenCache cache = cacheFactory.CreateForUser(Request.HttpContext.User);
AuthenticationContext authContext = new AuthenticationContext(_authority, cache);
// guid of the user currently logged into the app
string objectID = _userObjectId;
UserIdentifier userIdentifier = new UserIdentifier(objectID, UserIdentifierType.UniqueId);
string resource = "https://test.sharepoint.com";
AuthenticationResult result = await authContext.AcquireTokenSilentAsync(resource, credential, userIdentifier);
return result.AccessToken;
}
catch (Exception ex)
{
throw ex;
}
}
If I understand the flow correctly, the web app here will request a token using it's own credentials on behalf of the user currently logged in. (Am I right in understanding this based on the method signature which states - Identifier of the user token is requested for. This parameter can be Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier.Any.)
Now when this fails, I would need to make a call to AcquireTokenAsync. There are a number of these methods available and I can't seem to find the one that will fulfill this requirement.
Before the suggestion comes, I can't use AcquireTokenAsync(resource, clientId, redirectUri,new PlatformParameters(PromptBehavior.Auto)); because the constructor on PlatformParameters has changed and requires an implementation of a ICustomWebUi and this flow isn't supported on .Net Core 3.1 as far as I'm aware which makes this unusable.
AcquireTokenAsync(resource, credentials) works and returns a token, however, when using that token I get a 401 Unauthorized when accessing the Sharepoint resources, most likely because the token is different and it is now requested on behalf of the application and not the user logged into the application (if I'm following this train of thought correctly...).
My question is - which method do I call? Is there something I would need to add before making the call to AcquireTokenAsync and if so, which of the 10 or so overloads should I use? I tried using AcquireTokenAsync(resource, credenetial, userAssertion) and passed in the AccessToken that I retrieved on the User logged in, but then I got Assertion failed signature validation or variations on that. If I understood correctly, the UserAssertion can be initialized with 1,2 or 3 parameters and I tried providing the AccessToken currently on the user that is logged in the app, but with no success.
Any help is greatly appreciated as I've been looking at this for two days now.
I spent more time investigating this, but none of the methods available would have worked in my case. The auth flow wasn't an on-behalf-of flow, but an auth-code flow. The link is to the newer MSAL library, but the concept is there. The application, a .net core web app, directs the user to sign in. When they sign in, an auth-code is passed into the response once they successfully authenticate.
The auth-code is then used to call AcquireTokenByAuthorizationCodeAsync(AuthCode, Uri, ClientCredential, UserIdentifier). This returns the valid access token that can be stored in the distributed token cache and then used to authenticate in order to access a given resource.
My biggest issue was that the error says you need to use AcquireTokenAsync to retrieve a new token. This is correct to a certain point, because in order to make any calls to any of the 14 or so methods you will need different bits of information, which will be dependent on the way you have setup your authentication flow in your application.
Because the application I worked on used auth code flow, I would need to get a new auth code. This would mean redirecting the user to login, capture the auth code in the response if the login was successful and then call the appropriate AcquireTokenAsync method that takes in an auth code as parameter along with app info, uri and so on.
To solve this, I used the information provided by the Microsoft Github page on Acquiring tokens with auth codes in web apps. Here I found samples on how auth flow is setup, but most importantly, how to trigger a new authentication flow if the user needs to be re-authenticated.
I wrapped the code that would throw the AdalSilentTokenAcquisitionException, catch the error and return a RedirectToAction.
return RedirectToAction("ActionName", "Controller", new RouteValues);
The above redirects the user to a given action, in a particular controller and passes through an object that can hold additional parameters. In my case it's a new { redirectUri = redirectUriString}, which is a string object that holds the URL the user would try to navigate this. I constructed this with a little method that uses the current HttpRequest to find the url the user was trying to get to.
Next, the controller that I had setup which responds to that redirect:
[HttpGet("/SignIn")]
public IActionResult SignIn([FromQuery(Name ="redirectUri")]string redirectUri)
{
return Challenge
(
new AuthenticationProperties { RedirectUri = WebUtility.UrlDecode(redirectUri) },
OpenIdConnectDefaults.AuthenticationScheme
);
}
Here, a Challenge is returned. The challenge triggers a call to the authentication flow that was setup in the Startup class. I think the entire flow here is that the method will send people to go through whatever is in that startup, which, in the case of the application I worked on, it prompts the user to sign in, captures the auth code, requests a new access token and once this is received and saved in the distributed token cache, the user is redirected to the redirectUri that I passed through.
I hope this helps or at least gives a starting point to anyone who might encounter a similar issue.

How do I fix/avoid AADSTS650051: 'dynamicPermissions' is not a valid parameter for 'consentToApp' that some users get when trying to login with AAD?

I have a bare-bones website (single page app) that tries to log in the user with AAD using the msal JavaScript library. It is practically just doing what the example AAD login code does:
It creates a UserAgentApplication with my app's client ID and the authority URL for my tenant
It calls handleRedirectCallback and loginRedirect
It tries to get either the accessToken or the errorCode/errorMessage from the redirect response
Under practically all circumstances this works fine. Users visit my page, they get redirected and login just fine. One particular user, however, after the redirect and attempt to login gets this error:
Login failed: invalid_client - AADSTS650051: The parameter 'dynamicPermissions' in the request payload is not a valid parameter for the function import 'consentToApp'.
Trace Id: ed33266a-26ac-4706-9018-e6e89f650100
Correlation Id: e3103cab-1a7f-4a99-8455-fd8c8a769e35
Timestamp: 2019-06-25 20:50:44Z
He has tried this in many different browser (Edge/Chrome) and always gets this error, even in InPrivate/Incognito mode. No other user ever runs into error that I've found.
I'm not sure how to debug the issue because in my code I don't ever specify a 'dynamicPermissons' property or reference a function named 'consentToApp.'
How can I troubleshoot what is causing this error for this one user?
Thanks!
Ultimately this turned out to be a bug in the AAD service that was fixed by Microsoft.

chrome.identity.LaunchWebAuthFlow: How to implement logout in a web app using oauth2

I am working on some client side web app like a chrome extension that needs access to outlook mail and calendar. I followed the instruction on https://dev.outlook.com/RestGettingStarted and successfully got access and refresh tokens to retrieve data.
However, I cannot find any way of implementing "logout". The basic idea is to let user sign out and login with a different outlook account. In order to do that, I removed cached tokens, requested access tokens in interactive mode. The login window did pop out, but it took any valid email address, didn't let me input password and finally returned tokens for previous account. So I was not able to really use a different account until the old token expired.
Can anyone please tell me if it is possible to send a request to revoke the tokens so people can use a different account? Thanks!
=========================================================
Update:
Actually it is the fault of chrome.identity api. I used chrome.identity.LaunchWebAuthFlow to init the auth flow. It caches user's identity but no way to remove it. So we cannot really "logout" if using this api.
I used two logouts via launchWebAuthFlow - first I called the logout link to my app, then secondly, I called the logout link to Google.
var options = {
'interactive': false,
'url': 'https://localhost:44344/Account/Logout'
}
chrome.identity.launchWebAuthFlow(options, function(redirectUri) {});
options = {
'interactive': false,
'url': 'https://accounts.google.com/logout'
}
chrome.identity.launchWebAuthFlow(options, function(redirectUri) {});

.NET Gmail OAuth2 for multiple users

We are building a solution that will need to access our customers Gmail accounts to read/send mail. On account signup, we'd have a pop-up for our customer to do Gmail auth page and then a backend process to periodically read their emails.
The documentation doesn't seem to cover this use case. For example https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth says that client tokens should be stored in client_secrets.json - what if we have 1000s of clients, what then?
Service accounts are for non-user info, but rather application data. Also, if I use the GoogleWebAuthorizationBroker and the user has deleted access or the tokens have expired, I don't want my backend server app to pop open a web brower, as this seems to do.
I would imagine I could use IMAP/SMTP accomplish this, but I don't think it's a good idea to store those credentials in my db, nor do I think Google wants this either.
Is there a reference on how this can be accomplished?
I have this same situation. We are planning a feature where the user is approving access to send email on their behalf, but the actual sending of the messages is executed by a non-interactive process (scheduled task running on an application server).
I think the ultimate answer is a customized IAuthorizationCodeFlow that only supports access with an existing token, and will not execute the authorization process. I would probably have the flow simulate the response that occurs when a user clicks the Deny button on an interactive flow. That is, any need to get an authorization token will simply return a "denied" AuthorizationResult.
My project is still in the R&D phase, and I am not even doing a proof of concept yet. I am offering this answer in the hope that it helps somebody else develop a concrete solution.
While #hurcane's answer more than likely is correct (haven't tried it out), this is what I got working over the past few days. I really didn't want to have to de/serialize data from the file to get this working, so I kinda mashed up this solution
Web app to get customer approval
Using AuthorizationCodeMvcApp from Google.Apis.Auth.OAuth2.Mvc and documentation
Store resulting access & refresh tokens in DB
Use AE.Net.Mail to do initial IMAP access with access token
Backend also uses AE.Net.Mail to access
If token has expired, then use refresh token to get new access token.
I've not done the sending part, but I presume SMTP will work similarly.
The code is based on SO & blog posts:
t = EF object containing token info
ic = new ImapClient("imap.gmail.com", t.EmailAddress, t.AccessToken, AuthMethods.SaslOAuth, 993, true);
To get an updated Access token (needs error handling) (uses the same API as step #1 above)
using (var wb = new WebClient())
{
var data = new NameValueCollection();
data["refresh_token"] = refresh;
data["client_id"] = "(Web app OAuth id)";
data["client_secret"] = "(Web app OAuth secret)";
data["grant_type"] = "refresh_token";
var response = wb.UploadValues(#"https://accounts.google.com/o/oauth2/token", "POST", data);
string Tokens = System.Text.Encoding.UTF8.GetString(response);
var token = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(Tokens);
at = token.access_token;
return at;
}

How to use ADAL.js and enable a secure CORS call to Web API and Azure AD

I have a single page application needs to call a CORS enable web api. Both applications are secured by AAD. I found a sample done by Mat Velloso at https://github.com/matvelloso/AngularJSCORS
I've followed the steps in the readme file, only thing not sure was "remember to use the class ID from the client application you created" in step 2, I used a newly generate GUID. But I'm keep getting: XMLHttpRequest cannot load http://localhost:63918/api/values. The request was redirected to 'https://login.windows.net/devazureadnrw.onmicrosoft.com/wsfed?wa=wsignin1.0…3d0%26id%3dpassive%26ru%3d%252fapi%252fvalues&wct=2015-01-09T11%3a37%3a19Z', which is disallowed for cross-origin requests that require preflight.
Follwing is the Chrome console out:
renewToken is called for resource:http://localhost:63918/
adal.js:959 Add adal frame to document:adalRenewFrame
adal.js:959 Renew token Expected state: 9964340c-3c3b-4a2a-b710-f0d44f58655a|http://localhost:63918/
adal.js:755 Navigate url:https://login.windows.net/devazureadnrw.onmicrosoft.com/oauth2
authorize?re…e=9964340c-3c3b-4a2a-b710-f0d44f58655a%7Chttp%3A%2F%2Flocalhost%3A63918%2F
adal.js:959 Navigate to:https://login.windows.net/devazureadnrw.onmicrosoft.com/oauth2
authorize?re….onmicrosoft.com&domain_hint=devazureadnrw.onmicrosoft.com&nonce=undefined
adal.js:959 Add adal frame to document:adalRenewFrame
adal.js:959 State: 9964340c-3c3b-4a2a-b710-f0d44f58655a|http://localhost:63918/
adal.js:959 State status:true
adal.js:959 State is right
adal.js:959 Fragment has access token
adal.js:959 State: 9964340c-3c3b-4a2a-b710-f0d44f58655a|http://localhost:63918/
adal.js:959 State status:true
adal.js:959 State is right
adal.js:959 Fragment has access token
adal.js:959 Add adal frame to document:undefined
(index):1 XMLHttpRequest cannot load http://localhost:63918/api/values. The request was redirected to https://login.windows.net/devazureadnrw.onmicrosoft.com/wsfed?wa=wsignin1.0…3d0%26id%3dpassive%26ru%3d%252fapi%252fvalues&wct=2015-01-09T11%3a37%3a19Z', which is disallowed for cross-origin requests that require preflight.
adal.js:959 Add adal frame to document:undefined
adal.js:959 State: 9964340c-3c3b-4a2a-b710-f0d44f58655a|http://localhost:63918/
adal.js:959 State status:true
adal.js:959 State is right
adal.js:959 Fragment has access token
adal.js:959 State: 9964340c-3c3b-4a2a-b710-f0d44f58655a|http://localhost:63918/
adal.js:959 State status:true
adal.js:959 State is right
adal.js:959 Fragment has access token
The CORS pre flight request was made with 200 status code:
> values/api OPTIONS 200 OK text/plain angular.js:8560 Script 461 B 0 B
> 5 ms 4 ms
> authorize?response_type=token&client_id=1208eac1-f4dd-42f5-be33-886075f81be2&resource=http%3A%2F%2Flocalhost%3A63918%2F&redirect_uri=http%3A%2F%2Flocalhost%3A44302%2F&state=9964340c-3c3b-4a2a-b710-f0d44f58655a%7Chttp%3A%2F%2Flocalhost%3A63918%2F&prompt=none&login_hint=binjie%40devazureadnrw.onmicrosoft.com&domain_hint=devazureadnrw.onmicrosoft.com&nonce=undefined
> login.windows.net/devazureadnrw.onmicrosoft.com/oauth2 GET 302 Found
> text/html adal.js:297 Script
> 3.6 KB 0 B
> 1.30 s
> 1.29 s values/api GET 302 Found application/json Other 677 B 0 B 13 ms 13 ms
I'm stuck since this is the only sample I found. Any suggestions please? Many thanks.
This is Mat Velloso, I created that sample so you can shoot me :)
Here's what's going on: You are doing the CORS call, but the server is refusing it and asking you to authenticate. This can be for either of these three reasons (unless I'm missing something):
1-Either the Web API hasn't been properly configured to allow CORS (check my sample and the notes, I had exactly the same error at first because I didn't configure my Web API for it
2-Either you don't have a valid access token (which could be because the apps in AAD haven't been configured so one is allowed to call another, or just because you don't really have a valid access token)
3-Or either ADAL is not fetching the right access token for you because you didn't configure the endpoints collection (check how I initialize that in my app JavaScript, clearing out the right URLs)
Let me know if this helps, feel free to ping me for more info.
Regards,
Mat
sorry for the delay. I think I'mt not getting notifications so I missed it. Answering your questions:
1-The Guid is generated when you go to Azure Active Directory and create an app. It then assigns a new Guid as the client ID which identifies that's your app. Don't create a new Guid yourself, use the one identified as Client ID there.
2-Whether the app is deployed on Azure or running locally (or running anywhere else) is irrelevant. As long as you configure the reply URI correctly so the redirection falls where it is, you're good to go.
3-Once your user authenticates and you get a token back, whatever you do with that token is already on behalf of that user. So for example you can query the Azure Graph API and check in which groups that user is. The graph API is a REST api that lets you talk to Azure Active Directory. In order for that to work, keep in mind you need to give the app permissions to call Azure Graph API and read this sort of settings. Also remember that for every endpoint you call you need a specific access token (the one you get out of the authentication is only good for going back to AAD and asking for specific access tokens for specific things you want to access).
Perhaps a good way for you to start is taking a look at this: http://azure.microsoft.com/en-us/documentation/articles/mobile-services-how-to-register-active-directory-authentication/

Resources