根据配置的身份验证方式,JWT 或 AzureAD 身份验证适用于受保护的端点,但其中之一总是失败,并显示 “对象引用未设置到对象的实例”。
我已经像这样配置了我的 dotnet core Web api:
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var jwtSettings = config.GetSection("SecuritySettings:JwtSettings").Get<JwtSettings>();
byte[] key = Encoding.ASCII.GetBytes(jwtSettings.Key);
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
RoleClaimType = ClaimTypes.Role,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
context.HandleResponse();
if (!context.Response.HasStarted)
{
throw new UnauthorizedException("Authentication Failed.");
}
return Task.CompletedTask;
},
OnForbidden = _ => throw new ForbiddenException("You are not authorized to access this resource."),
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) &&
context.HttpContext.Request.Path.StartsWithSegments("/notifications"))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
})
.AddMicrosoftIdentityWebApi(config, "SecuritySettings:AzureAd", "AzureAD");
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
"AzureAD");
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
我打开了详细日志记录:
[14:36:59 DBG] Entity Framework Core 7.0.12 initialized 'ApplicationDbContext' using provider 'Npgsql.EntityFrameworkCore.PostgreSQL:7.0.11+c25a0d7b68d66c6ab3849a3b4333964faea6adc9' with options: SensitiveDataLoggingEnabled MigrationsAssembly=Migrators.PostgreSQL
[14:37:26 DBG] Compiling query expression:
'DbSet<ApplicationUser>()
.Where(u => u.ObjectId == __objectId_0)
.FirstOrDefault()'
[14:37:26 DBG] Generated query execution expression:
'queryContext =>
{
queryContext.AddParameter(
name: "__ef_filter__Id_0",
value: (object)Invoke(queryContext => ((ApplicationDbContext)queryContext.Context).TenantInfo.Id, queryContext));
return ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync<ApplicationUser>(
asyncEnumerable: new SingleQueryingEnumerable<ApplicationUser>(
(RelationalQueryContext)queryContext,
RelationalCommandCache.QueryExpression(
Projection Mapping:
EmptyProjectionMember -> Dictionary<IProperty, int> { [Property: ApplicationUser.Id (string) Required PK AfterSave:Throw, 0], [Property: ApplicationUser.AccessFailedCount (int) Required, 1], [Property: ApplicationUser.AuthenticationType (AuthenticationType?), 2], [Property: ApplicationUser.ConcurrencyStamp (string) Concurrency, 3], [Property: ApplicationUser.Email (string) MaxLength(256), 4], [Property: ApplicationUser.EmailConfirmed (bool) Required, 5], [Property: ApplicationUser.FirstName (string), 6], [Property: ApplicationUser.ImageUrl (string), 7], [Property: ApplicationUser.IsActive (bool) Required, 8], [Property: ApplicationUser.LastName (string), 9], [Property: ApplicationUser.LockoutEnabled (bool) Required, 10], [Property: ApplicationUser.LockoutEnd (DateTimeOffset?), 11], [Property: ApplicationUser.NormalizedEmail (string) Index MaxLength(256), 12], [Property: ApplicationUser.NormalizedUserName (string) Index MaxLength(256), 13], [Property: ApplicationUser.ObjectId (string) MaxLength(256), 14], [Property: ApplicationUser.PasswordHash (string), 15], [Property: ApplicationUser.PhoneNumber (string), 16], [Property: ApplicationUser.PhoneNumberConfirmed (bool) Required, 17], [Property: ApplicationUser.RefreshToken (string), 18], [Property: ApplicationUser.RefreshTokenExpiryTime (DateTime) Required, 19], [Property: ApplicationUser.SecurityStamp (string), 20], [Property: ApplicationUser.TenantId (string) Required Index MaxLength(64), 21], [Property: ApplicationUser.TwoFactorEnabled (bool) Required, 22], [Property: ApplicationUser.UserName (string) MaxLength(256), 23] }
SELECT TOP(1) u.Id, u.AccessFailedCount, u.AuthenticationType, u.ConcurrencyStamp, u.Email, u.EmailConfirmed, u.FirstName, u.ImageUrl, u.IsActive, u.LastName, u.LockoutEnabled, u.LockoutEnd, u.NormalizedEmail, u.NormalizedUserName, u.ObjectId, u.PasswordHash, u.PhoneNumber, u.PhoneNumberConfirmed, u.RefreshToken, u.RefreshTokenExpiryTime, u.SecurityStamp, u.TenantId, u.TwoFactorEnabled, u.UserName
FROM Identity.Users AS u
WHERE (u.TenantId == @__ef_filter__Id_0) && (u.ObjectId == @__objectId_0)),
null,
Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, ApplicationUser>,
HIS.Simple.Sync.Infrastructure.Persistence.Context.ApplicationDbContext,
False,
False,
True
),
cancellationToken: queryContext.CancellationToken);
}'
[14:37:34 ERR] Exception occurred while processing message.
System.NullReferenceException: Object reference not set to an instance of an object.
at lambda_method340(Closure, QueryContext)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at HIS.Simple.Sync.Infrastructure.Identity.UserService.GetOrCreateFromPrincipalAsync(ClaimsPrincipal principal) in C:\xxxxxxxxxx\src\Infrastructure\Identity\UserService.CreateUpdate.cs:line 31
at HIS.Simple.Sync.Infrastructure.Auth.AzureAd.AzureAdJwtBearerEvents.TokenValidated(TokenValidatedContext context) in C:\xxxxxxxxxx\src\Infrastructure\Auth\AzureAd\AzureAdJwtBearerEvents.cs:line 92
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
[14:37:34 ERR] Authentication failed Exception: System.NullReferenceException: Object reference not set to an instance of an object.
at lambda_method340(Closure, QueryContext)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at HIS.Simple.Sync.Infrastructure.Identity.UserService.GetOrCreateFromPrincipalAsync(ClaimsPrincipal principal) in Cxxxxxxxxxxsrc\Infrastructure\Identity\UserService.CreateUpdate.cs:line 31
at HIS.Simple.Sync.Infrastructure.Auth.AzureAd.AzureAdJwtBearerEvents.TokenValidated(TokenValidatedContext context) in C:\xxxxxxxxxx\src\Infrastructure\Auth\AzureAd\AzureAdJwtBearerEvents.cs:line 92
at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
错误发生在 Microsoft.EntityFrameworkCore.Query.Internal QueryCompiler.cs 的 ExecuteAsync 函数中:
移除另一个后,两者都可以工作。
在所述端点执行的查询:
public async Task<UserDetailsDto> GetAsync(string userId, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(userId);
_ = user ?? throw new NotFoundException(_t["User Not Found."]);
return user.Adapt<UserDetailsDto>();
}
我也调试过这个,
_userManager
和userId都不为null。
我在这个项目中使用了 fullstack Hero Web api 模板。
我尝试过调试,我尝试过切换配置,我尝试过使用不同的配置,例如
AddOpenIdConnect
,但仍然是相同的错误。
身份验证应同时使用 JWT 和 AzureAD。我应该能够使用两者生成的令牌进行身份验证。
编辑:事实证明这是 Finbuckle.Multitenant 的问题,但是仍然需要帮助,因为我无法解决该问题。
事实证明,这是一个 Finbuckle.Multitenant 的问题,它不支持使用 .WithClaimStrategy 和多个身份验证方案。
我使用了自定义策略来解决这个问题:
public class MultiAuthClaimStrategy : IMultiTenantStrategy
{
private readonly string _tenantKey;
private readonly string[]? _authenticationSchemes;
public MultiAuthClaimStrategy(string template)
: this(template, null)
{
}
public MultiAuthClaimStrategy(string template, string[]? authenticationSchemes)
{
if (string.IsNullOrWhiteSpace(template))
throw new ArgumentException(nameof(template));
_tenantKey = template;
_authenticationSchemes = authenticationSchemes;
}
public async Task<string?> GetIdentifierAsync(object context)
{
if (!(context is HttpContext httpContext))
throw new MultiTenantException(null, new ArgumentException($@"""{nameof(context)}"" type must be of type HttpContext", nameof(context)));
if (httpContext.User.Identity is { IsAuthenticated: true })
return httpContext.User.FindFirst(_tenantKey)?.Value;
var schemeProvider = httpContext.RequestServices.GetRequiredService<IAuthenticationSchemeProvider>();
if (_authenticationSchemes != null && _authenticationSchemes.Length > 0)
{
foreach (var schemeName in _authenticationSchemes)
{
var authScheme = (await schemeProvider.GetAllSchemesAsync()).FirstOrDefault(x => x.Name == schemeName);
if (authScheme != null)
{
var identifier = await AuthenticateAndRetrieveIdentifier(httpContext, authScheme);
if (identifier != null) return identifier;
}
}
}
else
{
var authScheme = await schemeProvider.GetDefaultAuthenticateSchemeAsync();
if (authScheme != null)
{
var identifier = await AuthenticateAndRetrieveIdentifier(httpContext, authScheme);
if (identifier != null) return identifier;
}
}
return null;
}
private async Task<string?> AuthenticateAndRetrieveIdentifier(HttpContext httpContext, AuthenticationScheme authScheme)
{
var handler = (IAuthenticationHandler)ActivatorUtilities.CreateInstance(httpContext.RequestServices, authScheme.HandlerType);
await handler.InitializeAsync(authScheme, httpContext);
httpContext.Items[$"__tenant____bypass_validate_principal__"] = "true"; // Value doesn't matter.
var handlerResult = await handler.AuthenticateAsync();
httpContext.Items.Remove($"__tenant____bypass_validate_principal__");
return handlerResult.Principal?.FindFirst(_tenantKey)?.Value;
}
}
然后我将其添加为这样的策略:
.WithStrategy<MultiAuthClaimStrategy>(ServiceLifetime.Singleton, "myTenantKey", new[] { "AzureAd", BearerTokenDefaults.AuthenticationScheme })
然而,不是我找出了问题所在,而是 Github 上的 @achandlerwhite / AndrewTriesToCode,他是这个很棒的 .NET 多租户中间件库的所有者和维护者!