如何在 OpenID (MSAL) 身份验证期间访问数据库

How to access database during OpenID (MSAL) authentication

我已使用 this 示例通过 MSAL v2 库为 Azure AD 配置 OpendID 身份验证。它使用 AzureAdAuthenticationBuilderExtensions class 配置 OpenId 连接事件(代码如下所示)。

我想在这些事件中访问我的数据库 (EF Core) 以检查 tenantId 并添加一些自定义用户声明。问题是注入的数据库上下文 (services.AddDbContext()) 是一个作用域服务,不能在 Configure 方法中调用。

public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
        => builder.AddAzureAd(_ => { });

    public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
    {
        builder.Services.Configure(configureOptions);
        builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
        builder.AddOpenIdConnect();
        return builder;
    }

    public class ConfigureAzureOptions: IConfigureNamedOptions<OpenIdConnectOptions>
    {
        private readonly AzureAdOptions _azureOptions;

        public AzureAdOptions GetAzureAdOptions() => _azureOptions;

        public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
        {
            _azureOptions = azureOptions.Value;
        }

        public void Configure(string name, OpenIdConnectOptions options)
        {
            options.ClientId = _azureOptions.ClientId;
            options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
            options.UseTokenLifetime = true;
            options.CallbackPath = _azureOptions.CallbackPath;
            options.RequireHttpsMetadata = false;
            options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
            //var allScopes = $"{_azureOptions.Scopes} {_azureOptions.GraphScopes}".Split(new[] {' '});
            var allScopes = $"{_azureOptions.Scopes} https://graph.microsoft.com/.default".Split(new[] { ' ' }); ;
            foreach (var scope in allScopes) { options.Scope.Add(scope); }

            options.TokenValidationParameters = new TokenValidationParameters
            {
                // Ensure that User.Identity.Name is set correctly after login
                NameClaimType = "name",

                // 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,

                // If the app is meant to be accessed by entire organizations, add your issuer validation logic here.
                //IssuerValidator = (issuer, securityToken, validationParameters) => {
                //    if (myIssuerValidationLogic(issuer)) return issuer;
                //}
            };

            options.Events = new OpenIdConnectEvents
            {
                OnTicketReceived = context =>
                {
                    return Task.CompletedTask;
                },
                OnAuthenticationFailed = context =>
                {
                    context.Response.Redirect("/Home/Error");
                    Console.WriteLine(context.Exception.Message);
                    context.HandleResponse(); // Suppress the exception
                    return Task.CompletedTask;
                },

                OnAuthorizationCodeReceived = async (context) =>
                {
                    var code = context.ProtocolMessage.Code;
                    var identifier = context.Principal.Claims.FirstOrDefault(c => c.Type.Contains("objectidentifier")).Value;
                    var memoryCache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>();
                    //var graphScopes = _azureOptions.GraphScopes.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                    var graphScopes = new string[] { "https://graph.microsoft.com/.default" };


                    var cca = new ConfidentialClientApplication(
                        _azureOptions.ClientId,
                        $"{_azureOptions.Instance}{_azureOptions.TenantId}",
                        _azureOptions.BaseUrl + _azureOptions.CallbackPath,
                        new ClientCredential(_azureOptions.ClientSecret),
                        new SessionTokenCache(identifier, memoryCache).GetCacheInstance(), 
                        null);
                    //var result = await cca.AcquireTokenByAuthorizationCodeAsync(code, graphScopes);

                    var result = await cca.AcquireTokenByAuthorizationCodeAsync(code, graphScopes);

                    // Check whether the login is from the MSA tenant. 
                    // The sample uses this attribute to disable UI buttons for unsupported operations when the user is logged in with an MSA account.
                    var currentTenantId = context.Principal.Claims.FirstOrDefault(c => c.Type.Contains("tenantid")).Value;
                    if (currentTenantId == "9188040d-6c67-4c5b-b112-36a304b66dad")
                    {
                        // MSA (Microsoft Account) is used to log in
                    }

                    context.HandleCodeRedemption(result.AccessToken, result.IdToken);
                },
                // If your application needs to do authenticate single users, add your user validation below.
                //OnTokenValidated = context =>
                //{
                //    return myUserValidationLogic(context.Ticket.Principal);
                //}
            };
        }

        public void Configure(OpenIdConnectOptions options)
        {
            Configure(Options.DefaultName, options);
        }
    }
}

您可以创建一个新方法来配置服务,而不是在 Configure 方法中调用它,如您引用的同一示例的注释中所示。

public void ConfigureServices(IServiceCollection services)
  {
    services.AddDbContextPool<ApplicationDbContext>(options => 
      options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
  }

Github issue 中的用户似乎遇到了与您类似的问题。请看看这里的对话是否有帮助。

要访问 DbContext,您可以尝试 HttpContext.RequestServices

public void Configure(string name, OpenIdConnectOptions options)
{
    //your code
    options.Events = new OpenIdConnectEvents
    {
        OnTicketReceived = context =>
        {
            var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
            // If your authentication logic is based on users then add your logic here
            return Task.CompletedTask;
        },
        OnAuthenticationFailed = context =>
        {
            var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
            context.Response.Redirect("/Home/Error");
            context.HandleResponse(); // Suppress the exception
            return Task.CompletedTask;
        },
        OnAuthorizationCodeReceived = async (context) =>
        {
            var db = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
            var code = context.ProtocolMessage.Code;
            var identifier = context.Principal.FindFirst(Startup.ObjectIdentifierType).Value;
            var memoryCache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>();
            var graphScopes = _azureOptions.GraphScopes.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

            var cca = new ConfidentialClientApplication(
                _azureOptions.ClientId, 
                _azureOptions.BaseUrl + _azureOptions.CallbackPath,
                new ClientCredential(_azureOptions.ClientSecret),
                new SessionTokenCache(identifier, memoryCache).GetCacheInstance(), 
                null);
            var result = await cca.AcquireTokenByAuthorizationCodeAsync(code, graphScopes);

            // Check whether the login is from the MSA tenant. 
            // The sample uses this attribute to disable UI buttons for unsupported operations when the user is logged in with an MSA account.
            var currentTenantId = context.Principal.FindFirst(Startup.TenantIdType).Value;
            if (currentTenantId == "9188040d-6c67-4c5b-b112-36a304b66dad")
            {
                // MSA (Microsoft Account) is used to log in
            }

            context.HandleCodeRedemption(result.AccessToken, result.IdToken);
        },
        // If your application needs to do authenticate single users, add your user validation below.
        //OnTokenValidated = context =>
        //{
        //    return myUserValidationLogic(context.Ticket.Principal);
        //}
    };
}