从外部提供商验证后如何验证用户是否存在于 IdentityServer4 中?

How to validate if user exist inside IdentityServer4 after being authenticated from External Provider?

我正在尝试找到一种正确的方法,在从 Azure Active Directory 等外部身份提供者成功验证用户身份后,我可以在其中注入服务以验证用户是否存在或是否已在我的应用程序中注册。我想做的是将用户重定向到自定义错误页面或显示未经授权的消息,如果他的帐户尚未在我的应用程序中注册。

我尝试使用 IProfileService 接口,但它似乎不是正确的方法。

这是我的 Startup.cs 设置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services
        .AddIdentityServer()
        .AddDeveloperSigningCredential()
        .AddTestUsers(Config.GetUsers())
        .AddInMemoryIdentityResources(Config.GetIdentityResources())
        .AddInMemoryApiResources(Config.GetApiResources())
        .AddInMemoryClients(Config.GetClients()) // Client was configured with RequireConsent = false, EnableLocalLogin = false,
        .AddProfileService<ProfileService>()
        .Services.AddTransient<IUserRepository,UserRepository>();

    services.AddAuthentication()
        .AddOpenIdConnect("AAD", "Azure Active Directory", options =>
        {
            options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
            options.SignOutScheme = IdentityServerConstants.SignoutScheme;
            options.Authority = "https://login.microsoftonline.com/MyTenant";
            options.ClientId = "MyClientId";
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = false
            };

            options.GetClaimsFromUserInfoEndpoint = true;                    
        });
}


public class ProfileService : IProfileService
{
    private readonly IUserRepository _userRepository;

    public ProfileService(IUserRepository userRepository)
    {
        _userRepository = userRepository 
    }

    public Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        var user = _userRepository.FindByUser(context.Subject.Identity.Name);

        // This will display HTTP 500 instead of 401 
        if(user == null) throw new UnauthorizedAccessException("You're not registered");

        // I add custom claims here

        return Task.FromResult(0);
    }

    public Task IsActiveAsync(IsActiveContext context) => Task.FromResult(0);
}

有没有我可以使用的可用服务或接口,我可以在其中注入我的用户验证以及允许我在该服务中注入我的用户存储库?是否可以在IdentityServer4中注入这种进程?有人可以指出我使用 IdentityServer4 实现目标的正确方向吗?

注意: 假设我有 SPA 网络应用程序并且我有自己的独立注册机制。如果用户不存在,我不想重定向回我的 SPA,而是在 IdentityServer4 中处理它。顺便说一句,为简洁起见,上面的一些代码没有包括在内。

如果您使用 ASP.NET Identity,您可以在 AccountController 中的 ExternalLoginCallback 函数中编写自定义逻辑。从 Azure AD 获取 JWT 令牌后,您可以解码令牌,获取用户声明,例如 email/name :

if (remoteError != null)
{
    ErrorMessage = $"Error from external provider: {remoteError}";
    return RedirectToAction(nameof(Login));
}
// read external identity from the temporary cookie
var aadResult1 = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (aadResult1?.Succeeded != true)
{
    throw new Exception("External authentication error");
}

// retrieve claims of the external user
var externalUser = aadResult1.Principal;
if (externalUser == null)
{
    throw new Exception("External authentication error");
}

// retrieve claims of the external user
var claims = externalUser.Claims.ToList();

// try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
if (userIdClaim == null)
{
    userIdClaim = claims.FirstOrDefault(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier");
}
if (userIdClaim == null)
{
    throw new Exception("Unknown userid");
}

然后你可以在数据库中写入你的服务implement/logic来确认用户是否已经在数据库中。如果是,则登录用户;如果不是,则将用户重定向到 confirmation/register 视图。类似于:

// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync("YourProvider", userIdClaim.Value, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
    _logger.LogInformation("User logged in with {Name} provider.", "YourProvider");

    // delete temporary cookie used during external authentication
    await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

    return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
    return RedirectToAction(nameof(Lockout));
}
else
{
    // If the user does not have an account, then ask the user to create an account.
    ViewData["ReturnUrl"] = returnUrl;
    ViewData["LoginProvider"] = "YourProvider";
    var email = claims.FirstOrDefault(x => x.Type == ClaimTypes.Upn).Value;
    return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}

这取决于你如何link AD 用户到本地数据库用户。使用 Azure AD 的对象 ID 或 UPN。

IdentityServer4 QuickStart UI 配置为在通过外部提供商登录时自动配置本地用户帐户。这一切都在 ExternalController.Callback:

中处理
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = 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 = AutoProvisionUser(provider, providerUserId, claims);
}

在您的情况下,您可以执行任何需要执行的逻辑,而不是调用 AutoProvisionUser。由于这是正在执行的常规 MVC 操作,您可以将自己的 类 注入 ExternalController 的构造函数或 Callback 本身(使用 [FromServices])。以下是您可能想要进行的更改的粗略概念:

public async Task<IActionResult> Callback([FromServices] IUserRepository userRepository)
{
    ...

    // lookup our user and external provider info
    var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
    if (user == null)
    {
        // We don't have a local user.
        return RedirectToAction("SomeAction", "SomeController");
    }

    ...
}