[Bug] ConsentHandler in MVC Partial View Produces Malformed Redirect Uri
See original GitHub issueWhich version of Microsoft Identity Web are you using? Microsoft Identity Web 1.0.0
Where is the issue?
- Web app
- Sign-in users and call web APIs
- Token cache serialization
- Distributed caches
Is this a new or an existing app? c. This is a new app or an experiment.
Repro
StartUp.cs
services.Configure<AuthenticationOptions>(Configuration.GetSection("AzureAd"));
string[] initialScopes = Configuration.GetSection("ServerRequestApi:Scopes").Get<string[]>();
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddDistributedTokenCaches();
services.TryAddScoped<MicrosoftIdentityConsentAndConditionalAccessHandler>();
services.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = Configuration.GetConnectionString("DefaultConnection");
options.SchemaName = "dbo";
options.TableName = "TokenCache";
});
services.AddHttpClient<IApiHealthService, ApiHealthService>();
services.AddHttpContextAccessor();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
ApiHealthService.cs
[AuthorizeForScopes(ScopeKeySection = "ServerRequestApi:Scopes)")]
public class ApiHealthService : IApiHealthService
{
private readonly HttpClient _apiClient;
private readonly ILogger<ApiHealthService> _logger;
private readonly IConfiguration _configuration;
private readonly ITokenAcquisition _tokenAcquisition;
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler;
private readonly string _remoteServiceBaseUrl;
private readonly string[] _scopes;
public ApiHealthService(
HttpClient httpClient,
ILogger<ApiHealthService> logger,
IConfiguration configuration,
ITokenAcquisition tokenAcquisition,
MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_apiClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_tokenAcquisition = tokenAcquisition ?? throw new ArgumentNullException(nameof(tokenAcquisition));
_consentHandler = consentHandler ?? throw new ArgumentNullException(nameof(consentHandler));
_remoteServiceBaseUrl = _configuration.GetSection("ServerRequestApi").GetValue<string>("ApiEndpoint");
_scopes = _configuration.GetSection("ServerRequestApi:Scopes").Get<string[]>();
}
public async Task AuthorizeApiClient()
{
string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(_scopes).ConfigureAwait(false);
_apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
public async Task<IEnumerable<PipelineStatus>> GetPipelineStatus(bool showInactive)
{
await AuthorizeApiClient();
List<PipelineStatus> parsedPipelineStatus = null;
try
{
var jsonPipelineStatus = await _apiClient.GetStringAsync($"{_remoteServiceBaseUrl}Health/GetPipelineStatus?showInactive={showInactive}");
parsedPipelineStatus = JsonConvert.DeserializeObject<List<PipelineStatus>>(jsonPipelineStatus);
}
catch (Exception ex)
{
_consentHandler.HandleException(ex);
}
if (null == parsedPipelineStatus)
{
return new List<PipelineStatus>();
}
parsedPipelineStatus = parsedPipelineStatus.ToList();
return parsedPipelineStatus;
}
}
_Layout.cshtml
@inject IApiHealthService HealthSvc
@inject Microsoft.Identity.Web.MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
<...>
<div class="container body-content">
@{
try
{
var pipelineStatus = await HealthSvc.GetPipelineStatus(false);
if (pipelineStatus.Any())
{
foreach (var message in pipelineStatus)
{
<div class="alert alert-warning">
@message.Timestamp: @message.Message
</div>
}
}
}
catch (Exception ex)
{
ConsentHandler.HandleException(ex);
}
}
@RenderBody()
</div>
Expected behavior The user is redirected to the expected page.
Actual behavior
The redirect Uri returned by MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException (non-Blazor) is malformed, containing and extra /
. E.g.: Attempts to navigate to https://localhost:37289
redirects to https://localhost:37289//
, navigating to https://localhost:37289/VMRequests
redirects to https://localhost:37289//VMRequests/
To recreate:
- log in to the app normally (navigate to
https://localhost:37289
) - delete the token from the cache
- force a refresh while still on the main page (
https://localhost:37289
) - be redirected to
https://localhost:37289//
(you can see the request in the debug console. E.g.:https://localhost:37289/MicrosoftIdentity/Account/Challenge?redirectUri=https://localhost:37289//&scope=...
)
This also works with an InMemoryTokenCache
if you leave the browser open but restart the app.
Possible solution
Modify MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException
or MicrosoftIdentityConsentAndConditionalAccessHandler.CreateBaseUri
to not add in extra /
s if they were not present in the originating Uri.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:2
- Comments:6 (1 by maintainers)
Top GitHub Comments
Included in 1.1.0 release.
We have the same problem with malformed redirect URLs when hosting the blazor (server-side) web app in a directory / virtual path: The wrong URL looks like this: https://localhost:44398//sampleMicrosoftIdentity/Account/Challenge?redirectUri=https://localhost:44398//sample&scope=user.read openid offline_access profile&loginHint=&domainHint=&claims=&policy=