Предыстория
У меня есть приложение ASP.NET MVC5, которое я подключаю для использования аутентификации oauth2 / Azure AD. На самом деле это уже работает в нескольких средах (dev / qa / prod / localhost).
В Azure у меня есть регистрация приложения с несколькими возвращаемыми URI, примеры:
Мой конвейер развертывания устанавливает переменную конфигурации для изменения ReturnURI длякод oauth и все работает просто отлично.
Вот как настроен код. Мои внутренние контроллеры имеют атрибут Authorize, который, когда пользователь не аутентифицирован, перенаправляет их на /Account/Index
, где выполняется вызов oauth. Вот как я запускаю вызов oauth:
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties
{
RedirectUri = Url.Action("ValidateOAuth", "Account", new { returnUrl })
},
OpenIdConnectAuthenticationDefaults.AuthenticationType);
Пользователь перенаправляется в Microsoft для входа в систему и возвращается /Account/ValidateOAuth
. В этом действии я проверяю Request.IsAuthenticated
и, если это true , я создаю локальную переменную сеанса для дополнительной информации о пользователе и помещаю ее во внутренние страницы.
Проблема
Я столкнулся с проблемой в конкретном сценарии. В моей среде QA у меня есть две копии сайта. Один в базовом URI (https://mywebsite.qa.com
) и один в качестве дополнительного приложения в IIS, расположенного в https://mywebsite.qa.com/staging
. Это сделано для того, чтобы наши тестировщики могли развертывать ветви на / промежуточном сайте и тестировать их, не мешая нашим обычным пользователям.
Когда пользователь посещает промежуточный сайт, они перенаправляются на мое действие /staging/Account/Index
, а вызов перенаправляет вызов. их на сайт входа Azure AD. Там, когда они аутентифицируются, они перенаправляются обратно на мой сайт. ОДНАКО, они перенаправляются не на /staging/Account/ValidateOAuth
, как ожидалось. Вместо этого они перенаправляются на /
. Это заставляет их проходить один и тот же цикл проверки подлинности основного сайта QA.
Я могу воспроизвести это при локальном запуске, настроив IIS Express для размещения сайта на http://localhost:43000/staging
, а не на базовом URL. http://localhost:43000
. Я вижу точно такое же поведение, когда он перенаправляет на /
, и я получаю ошибку, потому что у меня нет локально размещенного там сайта.
Вот мой Startup.cs с конфигурацией oauth:
public class Startup
{
// The Client ID is used by the application to uniquely identify itself to Azure AD.
string clientId = ConfigurationManager.AppSettings["aad:ClientId"];
string clientSecret = ConfigurationManager.AppSettings["aad:ClientSecret"];
// RedirectUri is the URL where the user will be redirected to after they sign in.
string redirectUri = ConfigurationManager.AppSettings["aad:RedirectUri"];
// Tenant is the tenant ID (e.g. contoso.onmicrosoft.com, or 'common' for multi-tenant)
static string tenant = ConfigurationManager.AppSettings["aad:Tenant"];
// Authority is the URL for authority, composed by Microsoft identity platform endpoint and the tenant name (e.g. https://login.microsoftonline.com/contoso.onmicrosoft.com/v2.0)
string authority = string.Format(CultureInfo.InvariantCulture, ConfigurationManager.AppSettings["aad:Authority"], tenant);
static string graphScopes = ConfigurationManager.AppSettings["aad:GraphScopes"];
/// <summary>
/// Configure OWIN to use OpenIdConnect
/// </summary>
/// <param name="app"></param>
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
/// <summary>
/// Handle failed authentication requests by redirecting the user to the home page with an error in the query string
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
notification.Response.Redirect("/Errors/Error?message=" + notification?.Exception?.Message);
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var secret = clientSecret;
if (string.IsNullOrEmpty(secret))
{
string filePath = HostingEnvironment.MapPath(@"~/local.aadclientsecret.txt");
if (!File.Exists(filePath))
{
throw new FileNotFoundException("AAD Client Secret not found in Web.Config and local.aadclientsecret.txt not found on server!");
}
secret = File.ReadAllText(filePath);
if (string.IsNullOrEmpty(secret))
{
throw new SettingsPropertyNotFoundException("AAD Client not found in Web.Config and local.aadclientsecret.txt was empty!");
}
}
var idClient = ConfidentialClientApplicationBuilder.Create(clientId)
.WithTenantId(tenant)
.WithRedirectUri(redirectUri)
.WithClientSecret(secret)
.Build();
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
string alias = "";
object aliasObj = null;
if (userDetails.AdditionalData.TryGetValue(GraphHelper.Attributes.Alias, out aliasObj))
{
alias = aliasObj.ToString();
}
// Create a new identity and copy all the claims.
// Add in extra claims that are needed.
var id = new ClaimsIdentity(notification.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(notification.AuthenticationTicket.Identity.Claims);
id.AddClaim(new Claim("alias", alias));
notification.AuthenticationTicket = new AuthenticationTicket
(
new ClaimsIdentity(id.Claims, notification.AuthenticationTicket.Identity.AuthenticationType),
notification.AuthenticationTicket.Properties
);
}
catch (MsalException ex)
{
string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Errors/Error?message={message}&&debug={ex.Message}");
}
catch (Microsoft.Graph.ServiceException ex)
{
string message = "GetUserDetailsAsync threw an exception";
notification.HandleResponse();
notification.Response.Redirect($"/Errors/Error?message={message}&debug={ex.Message}");
}
}
}
Если я поставлю точки останова в уведомлениях, ни один из них не попадет (как и ожидалось), поскольку перенаправление из Azure даже не возвращает меня на мой веб-сайт, расположенный по адресу /staging
.
Вот мои значения конфигурации (чувствительные опущены):
<add key="aad:ClientId" value="*************" />
<add key="aad:Tenant" value="***********" />
<add key="aad:ClientSecret" value="***********" />
<add key="aad:Authority" value="https://login.microsoftonline.com/{0}/v2.0" />
<add key="aad:RedirectUri" value="http://localhost:44300" />
<add key="aad:GraphScopes" value="User.Read"/>
Попытка решения
Если я добавлю новый URI перенаправления в регистрацию приложения Azure, чтобы указать на http://localhost:44300/staging
, он будет успешноверните меня на мой локально размещенный сайт в подкаталоге / staging, но Request.IsAuthenticated
всегда ложно, и я застреваю в бесконечном цикле перенаправления. Меня отправляют в Azure для входа в систему, и он автоматически перенаправляет меня на мой сайт, который перенаправляет меня обратно на страницу входа в Azure, повторяйте навсегда.
Я также пытался сделать это в своей среде живого контроля качества. Я добавил URI перенаправления в Azure, чтобы перейти на https://mywebsite.qa.com/staging
, а затем изменил RedirectUri в моей конфигурации на тот же. Когда я захожу на сайт /staging
сейчас, он ведет себя так же. Меня перенаправляют в Azure для входа в систему, а затем обратно на /
вместо /staging/Account/ValidateOAuth
.
Help!
Я провел день в поисках и отладке ружья, нопонятия не имею, что вызывает это. Что я делаю не так?