带有 Azure AD 的身份服务器 4 - "We couldn't sign you in. Please try again."
Identity Server 4 with Azure AD - "We couldn't sign you in. Please try again."
我正在使用 .NET Core 3.1 和 Identity Server 4,并通过 OpenIdConnect 连接到 Azure AD。我正在使用 Vue.js 前端和 .NET Core API。 IdentityServer、前端和 API 都托管在同一台服务器(同一域)上。一切都使用 https。我首先使用带有 EF 模型的 Oracle 数据库,以及完全自定义的 IdentityServer 存储和自定义用户存储(我实现了接口)。我正在使用 IdentityServer 的 Quickstart,稍微编辑一下以连接我的自定义用户存储而不是测试用户。我在我的开发环境中 运行 这个。
如果我在 IdentityServer 中输入 url,我将被重定向到 Azure AD,成功登录,并显示此页面:
Grants - successful login
声明正在从 Azure AD 返回并且自动配置成功。成功写入数据库。
通过我的 JS 客户端验证身份服务器,重定向到 Azure AD,我登录,然后它重定向到 IdentityServer 的外部控制器,然后重定向回 Microsoft url,然后继续重复直到它最终失败使用此页面:
Sign-in failure from Azure AD
我猜是我在某处弄乱了重定向 uri。这是我的代码和 IdentityServer 日志:
IdentityServer Log
该记录块重复 6-10 次。最后没有错误或任何不同。
我不得不分解 C# 代码,因为站点无法处理我的长选项行之一。
IdentityServer Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10),
CookieSlidingExpiration = true
};
}).AddClientStore<ClientStore>()
.AddCorsPolicyService<CorsPolicyService>()
.AddResourceStore<ResourceStore>()
.AddPersistedGrantStore<PersistedGrantStore>()
.AddProfileService<UserProfileService>();
services.AddScoped<IUserStore, UserStore>();
if (env.IsDevelopment())
{
// not recommended for production
builder.AddDeveloperSigningCredential();
}
else
{
// TODO: Load Signing Credentials for Production.
}
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.windows.net/[authority]";
options.CallbackPath = "/callback-aad";
options.ClientId = "[ClientId]";
options.RemoteSignOutPath = "/signout-aad";
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SaveTokens = true;
options.SignedOutCallbackPath = "/signout-callback-aad";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
options.UsePkce = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
客户端 OIDC 配置:
const oidcSettings = {
authority: '[IdentityServerUrl]',
client_id: '[ClientId]',
post_logout_redirect_uri: '[front-end url]/logout-aad',
redirect_uri: '[front-end url]/callback-aad',
response_type: 'code',
save_tokens: true,
scope: 'openid profile',
}
正在为 ExternalController 调用回调方法:
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {@claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = await AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Azure AD 配置:
redirect uri: [IdentityServer url]/callback-aad
数据库table数据:
Client table IMG1
Client table IMG2
ClientScopes table
ClientRedirectUris table
如果您需要任何其他信息,请告诉我。谢谢
问题出在我的自定义 UserStore 中。我通过 Azure AD SubjectId 而不是 UserSubjectId 获取用户。所以在 ExternalController 中,ApplicationUser 对象出现为 null。它没有出现异常,而是不断返回到 Azure AD 以尝试再次获取用户,但显然这只会造成无限循环。我没想到要看那里,因为我的用户已成功配置 ID 和声明。
我正在使用 .NET Core 3.1 和 Identity Server 4,并通过 OpenIdConnect 连接到 Azure AD。我正在使用 Vue.js 前端和 .NET Core API。 IdentityServer、前端和 API 都托管在同一台服务器(同一域)上。一切都使用 https。我首先使用带有 EF 模型的 Oracle 数据库,以及完全自定义的 IdentityServer 存储和自定义用户存储(我实现了接口)。我正在使用 IdentityServer 的 Quickstart,稍微编辑一下以连接我的自定义用户存储而不是测试用户。我在我的开发环境中 运行 这个。
如果我在 IdentityServer 中输入 url,我将被重定向到 Azure AD,成功登录,并显示此页面:
Grants - successful login
声明正在从 Azure AD 返回并且自动配置成功。成功写入数据库。
通过我的 JS 客户端验证身份服务器,重定向到 Azure AD,我登录,然后它重定向到 IdentityServer 的外部控制器,然后重定向回 Microsoft url,然后继续重复直到它最终失败使用此页面:
Sign-in failure from Azure AD
我猜是我在某处弄乱了重定向 uri。这是我的代码和 IdentityServer 日志:
IdentityServer Log
该记录块重复 6-10 次。最后没有错误或任何不同。
我不得不分解 C# 代码,因为站点无法处理我的长选项行之一。
IdentityServer Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10),
CookieSlidingExpiration = true
};
}).AddClientStore<ClientStore>()
.AddCorsPolicyService<CorsPolicyService>()
.AddResourceStore<ResourceStore>()
.AddPersistedGrantStore<PersistedGrantStore>()
.AddProfileService<UserProfileService>();
services.AddScoped<IUserStore, UserStore>();
if (env.IsDevelopment())
{
// not recommended for production
builder.AddDeveloperSigningCredential();
}
else
{
// TODO: Load Signing Credentials for Production.
}
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.windows.net/[authority]";
options.CallbackPath = "/callback-aad";
options.ClientId = "[ClientId]";
options.RemoteSignOutPath = "/signout-aad";
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SaveTokens = true;
options.SignedOutCallbackPath = "/signout-callback-aad";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
options.UsePkce = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
客户端 OIDC 配置:
const oidcSettings = {
authority: '[IdentityServerUrl]',
client_id: '[ClientId]',
post_logout_redirect_uri: '[front-end url]/logout-aad',
redirect_uri: '[front-end url]/callback-aad',
response_type: 'code',
save_tokens: true,
scope: 'openid profile',
}
正在为 ExternalController 调用回调方法:
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {@claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = await AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Azure AD 配置:
redirect uri: [IdentityServer url]/callback-aad
数据库table数据:
Client table IMG1
Client table IMG2
ClientScopes table
ClientRedirectUris table
如果您需要任何其他信息,请告诉我。谢谢
问题出在我的自定义 UserStore 中。我通过 Azure AD SubjectId 而不是 UserSubjectId 获取用户。所以在 ExternalController 中,ApplicationUser 对象出现为 null。它没有出现异常,而是不断返回到 Azure AD 以尝试再次获取用户,但显然这只会造成无限循环。我没想到要看那里,因为我的用户已成功配置 ID 和声明。