[Bug] Inconsistent results using OBO flow
See original GitHub issueLogs and network traces Without logs or traces, it is unlikely that the team can investigate your issue. Capturing logs and network traces is described in Logging wiki.
Which version of MSAL.NET are you using? Microsoft.Identity.Client 4.34.0 Microsoft.Identity.Web 1.14.0
Platform .Net Framework 4.7.2
What authentication flow has the issue?
- Mobile
- Interactive
- Integrated Windows Authentication
- Username Password
- Device code flow (browserless)
- Web app
- Authorization code
- On-Behalf-Of
- Daemon app
- Service to Service calls
Other?
- Sign in with mobile app
- Call private API which creates confidential client application and calls AquireTokenOnBehalfOf
- Call downstream graph API (outlook)
Is this a new or existing app? a. The app is in production, and I have upgraded to a new version of MSAL.
Repro
public class MicrosoftService : IMicrosoftService
{
public IConfidentialClientApplication ConfidentialClientApplication { get; set; }
private readonly ICompanyUserRepo _userRepo;
private readonly IAuthenticationSettings _authenticationSettings;
private readonly IDatabaseSettings _databaseSettings;
public MicrosoftService(
ICompanyUserRepo userRepo,
IAuthenticationSettings configuration,
IDatabaseSettings databaseSettings)
{
_userRepo = userRepo;
_authenticationSettings = configuration;
_databaseSettings = databaseSettings;
ConfidentialClientApplication = BuildApp();
}
public async Task GetAccessTokenAndUpdateUser(string userId, IEnumerable<string> scopes, string jwt)
{
await AquireTokenOnBehalfOf(scopes, jwt);
var user = _userRepo.GetByIdAndPartitionKeyOrDefault(userId, userId);
user.MicrosoftIdentity = jwt;
_userRepo.Update(user);
}
public async Task<AuthenticationResult> GetToken(string userId, IEnumerable<string> scopes)
{
var user = _userRepo.GetByIdAndPartitionKeyOrDefault(userId, userId);
var jwt = user.MicrosoftIdentity;
return await AquireTokenOnBehalfOf(scopes, jwt);
}
public async Task RevokeToken(string userId, IEnumerable<string> scopes)
{
var user = _userRepo.GetByIdAndPartitionKeyOrDefault(userId, userId);
var authResult = await AquireTokenOnBehalfOf(scopes, user.MicrosoftIdentity);
await ConfidentialClientApplication.RemoveAsync(authResult.Account);
user.MicrosoftIdentity = null;
_userRepo.Update(user);
}
private async Task<AuthenticationResult> AquireTokenOnBehalfOf(IEnumerable<string> scopes, string jwt)
{
var userAssertion = new UserAssertion(jwt);
var res = await ConfidentialClientApplication.AcquireTokenOnBehalfOf(scopes, userAssertion).ExecuteAsync();
return res;
}
private IConfidentialClientApplication BuildApp()
{
// The application which retreives the auth token must also use the same client id and redirect url when requesting the token. The auth token from the response is then passed to the AquireTokenByAuthorisationCode method below.
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder
.Create(_authenticationSettings.MicrosoftRevokeAppId)
.WithRedirectUri(_authenticationSettings.EmailScanWebAppBaseUrl + _authenticationSettings.MicrosoftRedirectAction)
.WithClientSecret(_authenticationSettings.MicrosoftRevokeAppSecret)
.WithTenantId(_authenticationSettings.MicrosoftTenantId)
.Build();
IMsalTokenCacheProvider cosmosTokenCacheProvider = CreateCosmosTokenCacheSerialiser();
cosmosTokenCacheProvider.Initialize(app.UserTokenCache);
return app;
}
private IMsalTokenCacheProvider CreateCosmosTokenCacheSerialiser()
{
IServiceCollection services = new ServiceCollection().AddLogging();
var cosmosConnectionString = string.Format("AccountEndpoint={0};AccountKey={1};", _databaseSettings.RevokeGlobalDatabaseUri, _databaseSettings.RevokeGlobalDatabaseKey);
services.AddDistributedTokenCaches();
services.AddCosmosCache((CosmosCacheOptions cacheOptions) =>
{
cacheOptions.DatabaseName = _databaseSettings.RevokeGlobalDatabase;
cacheOptions.ContainerName = _authenticationSettings.CosmosMsalContainer;
cacheOptions.CreateIfNotExists = true;
cacheOptions.ClientBuilder = new CosmosClientBuilder(cosmosConnectionString);
});
IServiceProvider serviceProvider = services.BuildServiceProvider();
IMsalTokenCacheProvider msalTokenCacheProvider = serviceProvider.GetRequiredService<IMsalTokenCacheProvider>();
return msalTokenCacheProvider;
}
}
}
Expected behavior A token is retrieved which can be used to call the downstream api
Actual behavior Sometimes the token is retrieved, sometimes it returns this error instead:
AADSTS70000: The request was denied because one or more scopes requested are unauthorized or expired. The user must first sign in and grant the client application access to the requested scope.\r\nTrace ID: 19a4f1a4-09cd-4729-8ccb-b1299cc37600\r\nCorrelation ID: 34dc13fe-7c6e-49af-acdc-e85b62088927\r\nTimestamp: 2021-07-15 13:37:24Z
Possible solution
Additional context / logs / screenshots Add any other context about the problem here, such as logs and screenshots.
Issue Analytics
- State:
- Created 2 years ago
- Comments:35 (15 by maintainers)
Top GitHub Comments
There may be a different way consent and actual tokens are handled for Mail.Read between AAD (work and school) and MSA (personal) accounts. I’ll try to repro.
@SirElTomato @JoshB82 the scopes look good. In fact MSAL.NET always requests “openid profile offline_access” so that the cache works correctly, so you could use only “mail.read” if you wish.