I am trying to get the silent token request to initialize the ConfidentialClientApp object as in the 'Microsoft Graph SDK ASPNET Connect' project and outlined in Add sign-in with Microsoft to an ASP.NET web app
With my code mirroring the samples above, I expect that my call will return a successful result with an access to.
var result = await cca.AcquireTokenSilentAsync(graphScopes, cca.Users.First());
return result.AccessToken;
However, I get an error where it says the user needs to be authenticated. I am not sure what I am missing from the examples that make this work in them.
You can only acquire the token silently if there is already a cached token for that user that includes the scopes you're requesting (it can have more, but it needs to have at least what you've asked for).
This is why AcquireTokenSilentAsync should always be wrapped in a Try/Catch block. If it fails to find a matching token, you need to launch an interactive flow. Here is an example from the MSAL Wiki:
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenSilentAsync(scopes, app.Users.FirstOrDefault());
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync.
// This indicates you need to call AcquireTokenAsync to acquire a token
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
result = await app.AcquireTokenAsync(scopes);
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
catch (Exception ex)
{
ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
return;
}
if (result != null)
{
string accessToken = result.AccessToken;
// Use the token
}
Related
I am trying to implement an OBO flow through to the graph API on a middle-tier API (.NET 5.0) using MSAL.NET. I'm running into two frustrating problems, and I can't find anyone having similar problems, so I think I'm misunderstanding something!
Problem 1: Whenever I call MSAL's GetAccountAsync, it always returns null when there should be an account loaded.
Problem 2: Whenever I call MSAL's AcquireTokenSilent, I always get the error "No refresh token found in the cache." even though I got one.
Here's what I have:
Once the web app authenticates, it passes through the token to a graph auth endpoint on the API:
var authenticationResult = await ClaimHelper.ClientApplication.AcquireTokenByAuthorizationCode(GraphHelpers.BasicGraphScopes, context.Code).ExecuteAsync();
var apiUserSession = await CouncilWiseAPIHelper.APIClient.Graph.AuthoriseUserAsync(authenticationResult.AccessToken);
which seems to work fine, and passes through a JWT to the API auth endpoint. The API implements an MSAL Confidential Client application and uses the SetBeforeAccess/SetAfterAccess token cache methods to save the cache to a database.
_msalClient = ConfidentialClientApplicationBuilder.Create(_graphConfig.ClientId)
.WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
.WithClientSecret(_graphConfig.ClientSecret)
.Build();
SetSerialiser(serialiser);
public void SetSerialiser(MSALTokenCacheSerialiser serialiser)
{
_msalClient.UserTokenCache.SetBeforeAccessAsync(serialiser.BeforeAccessCallbackAsync);
_msalClient.UserTokenCache.SetAfterAccessAsync(serialiser.AfterAccessCallbackAsync);
}
And the serialiser methods look like this:
public async Task BeforeAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
GraphUserTokenCache tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
if (tokenCache == null)
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache != null)
{
notification.TokenCache.DeserializeMsalV3(tokenCache.Value);
}
}
public async Task AfterAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
if (!notification.HasTokens)
{
// Delete from the cache
await _graphUserTokenCacheRepository.Delete(notification.SuggestedCacheKey);
}
if (!notification.HasStateChanged)
{
return;
}
GraphUserTokenCache tokenCache;
if (notification.SuggestedCacheKey == notification.Account.HomeAccountId.Identifier)
{
tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
}
else
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache == null)
{
var cache = notification.TokenCache.SerializeMsalV3();
tokenCache = new GraphUserTokenCache
{
Id = notification.SuggestedCacheKey,
AccountIdentifier = notification.Account.HomeAccountId.ToString(),
Value = cache
};
await _graphUserTokenCacheRepository.Add(tokenCache);
}
else
{
await _graphUserTokenCacheRepository.Update(tokenCache.Id, notification.TokenCache.SerializeMsalV3());
}
}
I can see the token BeforeAccess and AfterAccess methods being called, and I can see the caches being created in the database (encryption has been removed while I'm trying to track down this issue). If I inspect the serialised token cache being saved, it NEVER has a refresh token populated, but if I inspect the requests with fiddler I can see a refresh token was indeed provided.
Finally, here is the code for retrieving the access token which is called whenever a graph request is made:
public async Task<AuthenticationResult> GetAccessToken(string accountId, string jwtBearerToken)
{
try
{
IAccount account = null;
if (accountId.IsNotNullOrEmpty())
{
account = await _msalClient.GetAccountAsync(accountId);
}
var scope = _graphConfig.Scopes.Split(' ');
if (account == null)
{
var result = await _msalClient.AcquireTokenOnBehalfOf(scope,
new UserAssertion(jwtBearerToken))
.ExecuteAsync();
return result;
}
else
{
var result = await _msalClient.AcquireTokenSilent(scope, account)
.ExecuteAsync();
return result;
}
}
catch (MsalClientException ex)
{
ex.CwApiLog();
return null;
}
catch(Exception ex)
{
ex.CwApiLog();
return null;
}
}
When it's called with the jwtBearerToken, it will successfully call AcquireTokenOnBehalfOf() and the token is cached and a result returned, but when I come back to retrieve the account via GetAccountAsync() it always returns null even though I can see the token cache was loaded in BeforeAccessCallbackAsync().
Also, even if I call AcquireTokenSilent() immediately after acquiring the obo token with the account it just returned, I will get an exception saying there is no refresh token in the cache.
I am totally lost on what I'm doing wrong here, any help would be greatly appreciated.
I recently ran into the same problem while running a long runing OBO flow, MSAL has recently implemented an interface ILongRunningWebApi for these use cases you can go and see this new documentation
Here is an extract:
One OBO scenario is when a web API runs long running processes on
behalf of the user (for example, OneDrive which creates albums for
you). Starting with MSAL.NET 4.38.0, this can be implemented as such:
Before you start a long running process, call:
string sessionKey = // custom key or null
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.InitiateLongRunningProcessInWebApi(
scopes,
userToken,
ref sessionKey)
.ExecuteAsync();
userToken is a user token used to call this web API. sessionKey will
be used as a key when caching and retrieving the OBO token. If set to
null, MSAL will set it to the assertion hash of the passed-in user
token. It can also be set by the developer to something that
identifies a specific user session, like the optional sid claim from
the user token (for more information, see Provide optional claims to
your app). If the cache already contains a valid OBO token with this
sessionKey, InitiateLongRunningProcessInWebApi will return it.
Otherwise, the user token will be used to acquire a new OBO token from
AAD, which will then be cached and returned.
In the long-running process, whenever OBO token is needed, call:
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.AcquireTokenInLongRunningProcess(
scopes,
sessionKey)
.ExecuteAsync();
Pass the sessionKey which is associated with the current user's
session and will be used to retrieve the related OBO token. If the
token is expired, MSAL will use the cached refresh token to acquire a
new OBO access token from AAD and cache it. If no token is found with
this sessionKey, MSAL will throw a MsalClientException. Make sure to
call InitiateLongRunningProcessInWebApi first.
Hope this helps :)
I am writing a console app using some code I found on GitHub. The code below works fine.
If the token is cached, it is retrieved and the application continues. (AcquireTokenSilent)
If no token is found then I prompt the user for their credentials. They're taken to the company login site. They login, and the application continues. (AcquireTokenInteractive)
However, let's say the user simply changes their mind and abandons the login process by closing web the browser. Now, my code is just sitting there. All you see is a command window doing nothing.
It's clearly waiting for some response, that will never come since the Web Browser will no longer be communicating with my Console App.
How would I receive a message from the closing Web Browser, so my Console App knows to throw an exception or process the abandoned login somehow?
namespace PublicClientAuthentication
{
class AuthenticationProvider : IAuthenticationProvider
{
IPublicClientApplication _clientApp;
private string[] _scopes;
public AuthenticationProvider(IPublicClientApplication app, string[] scopes)
{
_clientApp = app;
_scopes = scopes;
}
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
AuthenticationResult authResult = null;
try
{
var accounts = await _clientApp.GetAccountsAsync();
authResult = await _clientApp.AcquireTokenSilent(_scopes.ToArray(), accounts.FirstOrDefault()).ExecuteAsync().ConfigureAwait(false);
}
catch (MsalUiRequiredException ex)
{
System.Diagnostics.Debug.WriteLine(ex);
authResult = await _clientApp.AcquireTokenInteractive(_scopes)
.WithPrompt(Microsoft.Identity.Client.Prompt.ForceLogin)
.ExecuteAsync().ConfigureAwait(false);
}
catch (MsalServiceException ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
catch (MsalClientException ex)
{
System.Diagnostics.Debug.WriteLine(ex);
authResult = await _clientApp.AcquireTokenInteractive(_scopes).ExecuteAsync();
}
catch (Exception e)
{
System.Diagnostics.Debug.Write(e);
}
request.Headers.Add("Authorization", authResult.CreateAuthorizationHeader());
}
}
}
Adding WithUseEmbeddedWebView did the trick.
This code works in the .NET Framework. It does not work with .NET Core
authResult = await _clientApp.AcquireTokenInteractive(_scopes)
.WithUseEmbeddedWebView(true)
.WithPrompt(Prompt.ForceLogin)
.ExecuteAsync().ConfigureAwait(false);
I'm programatically start/stop Azure PowerBI capacity for PowerBI Embedded.
On button click , resume/suspend the powerbi embed service in Azure. I followed below link to do this.
https://learn.microsoft.com/en-us/rest/api/power-bi-embedded/capacities/resume
How to get authorization code dynamicallly each time i click the button.
You can get an access token for Power BI using Azure Active Directory Authentication Libraries. The easiest way to get it is to install Microsoft.IdentityModel.Clients.ActiveDirectory NuGet package. Then to obtain an access token you need to call AcquireTokenAsync method. Here is how you can do this:
private static string redirectUri = "https://login.live.com/oauth20_desktop.srf";
private static string resourceUri = "https://analysis.windows.net/powerbi/api";
private static string authorityUri = "https://login.windows.net/common/oauth2/authorize";
// Obtain at https://dev.powerbi.com/apps
private static string clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
private static AuthenticationContext authContext = new AuthenticationContext(authorityUri, new TokenCache());
private async void btnAuthenticate_ClickAsync(object sender, EventArgs e)
{
var authenticationResult = await authContext.AcquireTokenAsync(resourceUri, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Auto));
if (authenticationResult == null)
MessageBox.Show("Call failed.");
else
MessageBox.Show(authenticationResult.AccessToken);
}
The last parameter is PromptBehavior.Auto. This means that you will be prompted for credentials, unless your identity is saved on this computer. Also, when there is no consent for access is given this app, the user will be prompted too. The authentication is performed in an interactive way - it expect that there will be a human, who will enter credentials in case they are needed. If you want to obtain an access token in non-interactive way, you can use user name and password in your code. In this case the method for obtaining the access token should look like this:
private void btnAuthenticate_Click(object sender, EventArgs e)
{
AuthenticationResult authenticationResult = null;
// First check is there token in the cache
try
{
authenticationResult = authContext.AcquireTokenSilentAsync(resourceUri, clientId).Result;
}
catch (AggregateException ex)
{
AdalException ex2 = ex.InnerException as AdalException;
if ((ex2 == null) || (ex2 != null && ex2.ErrorCode != "failed_to_acquire_token_silently"))
{
MessageBox.Show(ex.Message);
return;
}
}
if (authenticationResult == null)
{
var uc = new UserPasswordCredential("user#example.com", "<EnterStrongPasswordHere>"); // Or parameterless if you want to use Windows integrated auth
try
{
authenticationResult = authContext.AcquireTokenAsync(resourceUri, clientId, uc).Result;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.InnerException == null ? "" : Environment.NewLine + ex.InnerException.Message);
return;
}
}
if (authenticationResult == null)
MessageBox.Show("Call failed.");
else
MessageBox.Show(authenticationResult.AccessToken);
}
Please note, that this call may fail, if no consent is given to your app. To do this, go to Azure Portal -> Azure Active Directory -> App registrations and locate your app. Then open your app's settings and in Required permissions select Power BI Service and click Grant permissions:
At this point you can use this access token to perform REST API calls or to embed elements in your app. This token gives access to everything which the user can access and it has been allowed to be accessed when you registered your app in the portal. If you want however to generate a token for one particular report (or tile, or dashboard), then you can call some of the Embed Token methods, e.g. GenerateTokenInGroup (using the ADAL access token to authenticate yourself in the headers of the request for generating the embedded token).
I'm implementing the authorization code flow for Installed Application.
Code is similar to snippet below:
public static void main(String[] args) {
try {
httpTransport = GoogleNetHttpTransport.newTrustedTransport();
dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR);
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(httpTransport,
JSON_FACTORY,
IA_CLIENT,
IA_SECRET,
Collections.singleton(DriveScopes.DRIVE)).setDataStoreFactory(dataStoreFactory)
.build();
Credential credential = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
drive = new Drive.Builder(httpTransport, JSON_FACTORY, credential).setApplicationName(APPLICATION_NAME).build();
System.out.println(drive.about());
return;
} catch (IOException e) {
System.err.println(e.getMessage());
} catch (Throwable t) {
t.printStackTrace();
}
System.exit(1);
}
Everything is working fine except in the case when I provide an invalid client_id. (I've the same issue if I use a json file and alter its content).
I get this get of error message from Google server:
401. That’s an error.
Error: invalid_client
The OAuth client was not found.
Request Details
client_id=573900000-hsoobsdsstem84tg8br4pmdsds.apps.googleusercontent.com
redirect_uri=http://localhost:40441/Callback
response_type=code
scope=https://www.googleapis.com/auth/drive
... and the callback server never receives any feedback. So, the application
is still running endlessly.
I've looked at the LocalServerReceiver class but could find any way to provide a
timeout or any potential solution.
What's the cleanest way to handle this case ?
I have successfully been able to get an access_token (or authenticationToken for Microsoft tokens) using the client side authentication in my Xamarin forms App. I am able to get further user information (email, name, etc.) using the same access token. Now, when I try to pass that token to my Azure Mobile Service backend, I get a 401 error.
Here is my code:
private async System.Threading.Tasks.Task<string> MSGetUserInfo(Account account)
{
// Reference: http://graph.microsoft.io/en-us/docs/overview/call_api
// Note that Microsoft don't recognize the access_token header entry, but rely instead on an Authorization header entry
var client = new HttpClient();
var userInfoRequest = new HttpRequestMessage()
{
RequestUri = new Uri("https://graph.microsoft.com/v1.0/me"),
Method = HttpMethod.Get,
};
// Add acccess Bearer
userInfoRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", account.Properties["access_token"]);
using (var response = await client.SendAsync(userInfoRequest).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
Models.User user = new Models.User();
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var jobject = JObject.Parse(responseString);
var userName = (string)jobject["userPrincipalName"];
// Check username is valid
if (String.IsNullOrEmpty(userName))
{
throw new Exception("Username was not set for authenticated user");
}
else
user.ProviderLoginId = userName;
var userDisplayName = (string)jobject["displayName"];
// Replace display name if invalid
if (String.IsNullOrWhiteSpace(userDisplayName))
{
userDisplayName = userName;
}
else
user.Name = userDisplayName;
var userEmail = (string)jobject["mail"];
// Replace email if invalid
if (String.IsNullOrWhiteSpace(userEmail))
{
userEmail = userName;
}
else
user.Email = userEmail;
Valufy.App.currentUser = user;
}
else
{
throw new Exception("OAuth2 request failed: " + await response.Content.ReadAsStringAsync().ConfigureAwait(false));
}
}
return "success";
}
The above code snippet works in getting my user details. Now when I try to use the same token in the subsequent call, I get a 404:
public async Task<bool> Authenticate(string token)
{
string message = string.Empty;
var success = false;
JObject objToken = new JObject();
//objToken.Add("access_token", token); //for facebook and google
objToken.Add("authenticationToken", token); //for microsoft
try
{
// Sign in with Facebook login using a server-managed flow.
if (user == null)
{
//ProviderAuth("MICROSOFT");
user = await syncMgr.CurrentClient
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount, objToken);
if (user != null)
{
success = true;
message = string.Format("You are now signed-in as {0}.", user.UserId);
}
}
}
catch (Exception ex)
{
message = string.Format("Authentication Failed: {0}", ex.Message);
}
// Display the success or failure message.
// await new MessageDialog(message, "Sign-in result").ShowAsync();
return success;
}
Is there something that I am doing wrong? Any and all assistance is appreciated.
According to your description, I followed this Git sample about Microsoft Graph Connect Sample for UWP (REST). I could get the access_token and it could work as expected with Microsoft Graph API (e.g. Get a user). But when I use this access_token as the authenticationToken token object for MobileServiceClient.LoginAsync, I could also get 401 Unauthorized.
Then I checked the managed client for Azure Mobile Apps about Authenticate users. For Client-managed authentication flow, I found that the official code sample about using Microsoft Account is working with Live SDK as follows:
// Request the authentication token from the Live authentication service.
// The wl.basic scope should always be requested. Other scopes can be added
LiveLoginResult result = await liveIdClient.LoginAsync(new string[] { "wl.basic" });
if (result.Status == LiveConnectSessionStatus.Connected)
{
session = result.Session;
// Get information about the logged-in user.
LiveConnectClient client = new LiveConnectClient(session);
LiveOperationResult meResult = await client.GetAsync("me");
// Use the Microsoft account auth token to sign in to App Service.
MobileServiceUser loginResult = await App.MobileService
.LoginWithMicrosoftAccountAsync(result.Session.AuthenticationToken);
}
Note: As LiveConnectSession states about AuthenticationToken:
The authentication token for a signed-in and connected user.
While check the authentication with Microsoft Graph, I could only find the access_token instead of AuthenticationToken.
UPDATE:
I have checked LiveLogin for WP8 and Microsoft Account Authentication for Mobile Apps via Fiddler to capture the authorize requests. I found that MS account authentication has the similar authorize request as Live SDK.
I assumed that you need to leverage Live SDK to authenticate the user when using client side authentication with Microsoft account. I found the Live SDK download page is not exist, you could follow the Live SDK for WP8 to get started with Live SDK.
UPDATE2:
For the client-flow authentication (Microsoft Account), you could leverage MobileServiceClient.LoginWithMicrosoftAccountAsync("{Live-SDK-session-authentication-token}"), also you could use LoginAsync with the token parameter of the value {"access_token":"{the_access_token}"} or {"authenticationToken":"{Live-SDK-session-authentication-token}"}. I have tested LoginAsync with the access_token from MSA and retrieve the logged info as follows: