Я бьюсь головой об этом уже несколько месяцев.До недавнего времени это не стало серьезной проблемой, и я надеюсь, что кто-нибудь сможет мне помочь.К сожалению, учитывая размер проекта, я не могу легко поделиться воспроизводимой версией.Тем не менее, надеюсь, что то, что у меня есть ниже, прояснит мою проблему, и вы увидите, где я допустил ошибку.
У меня есть два сайта: приложение ASP.Net Core MVC и сервер входа в систему, а также ASP.Net Core MVC.Давайте назовем их http://mvc.mysite.com и http://login.mysite.com. Ни то, ни другое существенно не отличается от IdentityServer4 Quickstart # 6.Единственная реальная разница в том, что я реализовал внешний поставщик входа в систему для AzureAd.Мой код для этого ниже.
Сценарий 1
Учитывая внутренний поток входа в систему, где пользователь использует внутреннюю страницу входа в систему в http://login.mysite.com все работаетштраф.
- посещения пользователя http://mvc.mysite.com/clients/client-page-1
- Пользователь перенаправлен на http://login.mysite.com/Account/Login
- Пользователь входит в систему с правильным именем пользователя / паролем
- Пользовательперенаправляется на http://mvc.mysite.com/clients/client-page-1
Сценарий 2
Однако, если метод AccountController :: Login () сервера входа в систему определяет, что существует один ExternalLoginProvider, и выполняетв строке «возвращать в ожидании ExternalLogin (vm.ExternalLoginScheme, returnUrl);», тогда первоначальный redirectUrl теряется.
посещения пользователя http://mvc.mysite.com/clients/client-page-1
Пользовательперенаправлен на http://login.mysite.com/Account/Login (получение вывода AccountController :: ExternalLogin)
Пользователь перенаправлен на внешний поставщик OIDC AzureAd
Пользователь входит в систему с правильным именем пользователя / паролем
Пользователь переадресованhttp://login.mysite.com/Account/ExternalLoginCallback
Пользователь перенаправлен на http://mvc.mysite.com (обратите внимание, что пользователь перенаправляется в корневой каталог сайта MVC вместо / clients / client-page-1)
Для сценария 1:
- С учетом сайта MVC
- При использовании отладчика для проверки контекста, предоставленного дляOpenIdConnectEvents (например, OnMessageReceived, OnUserInformationReceived и т. д.)
- Тогда все контексты имеют объект Properties, который содержит RedirectUri == «http://mvc.mysite.com/clients/client-page-1”
Для сценария 2:
- С учетом сайта MVC
- При использовании отладчика для проверки контекста, предоставленного OpenIdConnectEvents (например, OnMessageReceived, OnUserInformationReceived и т. Д.)
- Тогдавсе контексты имеют объект Properties, который содержит RedirectUri == «http://mvc.mysite.com” (отсутствует /client.client-page-1)
В Startup.cs моего сервера входа в систему я добавилэто для ConfigureServices:
services.AddAuthentication()
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
AzureAdOptions.Settings = options;
});
РеализацияОписание AddAzureAd выглядит следующим образом: (Вы увидите объекты опций, которые были переданы, я заменил все варианты использования постоянными значениями, кроме ClientId и ClientSecret).
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.AddOpenIdConnect("AzureAd", "Azure AD", options =>
{
var opts = new AzureAdOptions();
configureOptions(opts);
var config = new ConfigureAzureOptions(opts);
config.Configure(options);
});
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(AzureAdOptions azureOptions)
{
_azureOptions = azureOptions;
}
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions) : this(azureOptions.Value) {}
public void Configure(string name, OpenIdConnectOptions options)
{
Configure(options);
}
public void Configure(OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = "https://login.microsoftonline.com/common"; //_azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = "/signin-oidc"; // _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false; // true in production // _azureOptions.RequireHttps;
options.ClientSecret = _azureOptions.ClientSecret;
// Add code for hybridflow
options.ResponseType = "id_token code";
options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
};
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
/// <summary>
/// Redeems the authorization code by calling AcquireTokenByAuthorizationCodeAsync in order to ensure
/// that the cache has a token for the signed-in user.
/// </summary>
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
}
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
throw context.Exception;
}
}
}
public class NaiveSessionCache : TokenCache
{
private static readonly object FileLock = new object();
string UserObjectId = string.Empty;
string CacheId = string.Empty;
ISession Session = null;
public NaiveSessionCache(string userId, ISession session)
{
UserObjectId = userId;
CacheId = UserObjectId + "_TokenCache";
Session = session;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
Load();
}
public void Load()
{
lock (FileLock)
this.Deserialize(Session.Get(CacheId));
}
public void Persist()
{
lock (FileLock)
{
// reflect changes in the persistent store
Session.Set(CacheId, this.Serialize());
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
Session.Remove(CacheId);
}
public override void DeleteItem(TokenCacheItem item)
{
base.DeleteItem(item);
Persist();
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Load();
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
Persist();
}
}