如何正确设置 JwtBearerOptions

How to properly setup JwtBearerOptions

我设置 Identity Server 4 来发布 JWT 令牌来验证用户。在 Identity Server 4 中,我设置了以下内容:

public class Resources {

    public static IEnumerable<ApiResource> GetResources() {
        return new[] {
            new ApiResource {
                Name = "Test.API",
                DisplayName = "Test API",
                Description = "Allow the user access to the test API",
                Scopes = new List<string> { "Core API" },
                UserClaims = new List<string> { "General", "Admin" }
            }
        };
    }

    public static IEnumerable<ApiScope> GetScopes() {
        return new[] {
            new ApiScope("Core.API", "Allow access to the test API")
        };
    }

    public static IEnumerable<Client> GetClients() {
        return new List<Client>() {
            new Client {
                ClientName = "Test Client",
                ClientId = "b778a2ad-090d-4525-8954-6411de2cd339",
                ClientSecrets = new List<Secret> { new Secret("random_text".Sha512()) },
                AllowedScopes = new List<string> { "Core.API" },
                AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
            },
            new Client {
                ClientName = "Test Web App",
                ClientId = "abb9c89c-a018-4b0f-9a0f-4e701c637665",
                ClientSecrets = new List<Secret> { new Secret("other_random_text".Sha512()) },
                AllowedGrantTypes = GrantTypes.Hybrid,
                RequirePkce = false,
                AllowRememberConsent = false,
                AllowedScopes = new List<string>
                {
                    StandardScopes.OpenId,
                    StandardScopes.Profile,
                    StandardScopes.Address,
                    StandardScopes.Email,
                    "Core.API",
                    "roles"
                }
            }
        };
    }

    public static IEnumerable<IdentityResource> GetIdentities() {
        return new[] {
            new IdentityResources.Email(),
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResource {
                Name = "User Role",
                UserClaims = new List<string> { "Admin", "General" }
            }
        };
    }
}

public class Startup {

    public Startup(IConfiguration configuration, IWebHostEnvironment appEnv) {
        Configuration = configuration;
        CurrentEnvironment = appEnv;
    }

    public IConfiguration Configuration { get; }

    private IWebHostEnvironment CurrentEnvironment { get; set; }

    public void ConfigureServices(IServiceCollection services) {

        services.AddScoped<IUserRequester, UserRequester>(_ =>
            new UserRequester(Configuration.GetSection("AzureTableStore.UserLogin").Get<TableStoreConfiguration>()));
        services.AddControllers().AddNewtonsoftJson(options =>
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());

        IIdentityServerBuilder builder = services.AddIdentityServer();
        if (CurrentEnvironment.IsDevelopment()) {
            builder.AddDeveloperSigningCredential();
        } else {
            X509Certificate2 certData = DownloadCertificate(Configuration.GetSection("APICertificate").Get<Secret>());
            builder.AddSigningCredential(certData);
        }

        builder.AddInMemoryClients(Resources.GetClients());
        builder.AddInMemoryIdentityResources(Resources.GetIdentities());
        builder.AddInMemoryApiResources(Resources.GetResources());
        builder.AddInMemoryApiScopes(Resources.GetScopes());

        builder.Services.Configure<TableStoreConfiguration>(Configuration.GetSection("AzureTableStore.UserLogin"));
        builder.Services.Configure<RedisConfiguration>(Configuration.GetSection("RedisCache"));
        builder.Services.AddTransient<IRedisConnection, RedisConnection>();
        builder.Services.AddTransient<IUserRequester, UserRequester>();
        builder.Services.AddTransient<IProfileService, ProfileService>();
        builder.Services.AddTransient<IResourceOwnerPasswordValidator, PasswordValidator>();
        builder.Services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
        builder.Services.AddTransient<IReferenceTokenStore, ReferenceTokenStore>();
        builder.Services.AddTransient<IRefreshTokenStore, RefreshTokenStore>();
        builder.Services.AddTransient<IUserConsentStore, UserConsentStore>();

        services.AddSwaggerGen(c => {
            c.SwaggerDoc("v1", new OpenApiInfo {
                Version = "v1",
                Title = "Authentication",
                Description = "API allowing for user requests to be authenticated against their credentials",
                Contact = new OpenApiContact {
                    Name = "Me",
                    Email = "me@fake.com"
                }
            });

            string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            string xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
            c.IncludeXmlComments(xmlPath);
        });

        services.AddCors(options => options.AddDefaultPolicy(
            builder => builder.AllowAnyOrigin().
                SetIsOriginAllowedToAllowWildcardSubdomains().
                AllowAnyMethod().
                AllowAnyHeader().
                WithHeaders("X-TEST", "true")));
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
        if (env.IsDevelopment()) {
            app.UseDeveloperExceptionPage();
        }

        app.UseSwagger();
        app.UseSwaggerUI(c =>
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "Authentication API v1"));

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseCors();
        app.UseIdentityServer();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller}/{action=Index}/{id?}"));
    }

    private static X509Certificate2 DownloadCertificate(Secret secret) {

        KeyVaultSecret secretValue = new Provider(secret.KeyVaultName).GetSecretAsync(secret.SecretName).Result;

        var store = new Pkcs12Store();
        using (Stream stream = secret.KeyVaultName.Equals("local")
            ? new FileStream(Environment.GetEnvironmentVariable(secret.SecretName), FileMode.Open)
            : new MemoryStream(Convert.FromBase64String(secretValue.Value))) {
            store.Load(stream, Array.Empty<char>());
        }

        string keyAlias = store.Aliases.Cast<string>().SingleOrDefault(a => store.IsKeyEntry(a));
        var key = (RsaPrivateCrtKeyParameters)store.GetKey(keyAlias).Key;
        var certificate = new X509Certificate2(
            DotNetUtilities.ToX509Certificate(store.GetCertificate(keyAlias).Certificate));
        var rsa = new RSACryptoServiceProvider();
        rsa.ImportParameters(DotNetUtilities.ToRSAParameters(key));
        return RSACertificateExtensions.CopyWithPrivateKey(certificate, rsa);
    }
}

在我所有服务的 Startup.cs 中,我有以下内容:

services.AddAuthentication(configuration.SchemeType).
    AddJwtBearer("Bearer", options => {
        options.Authority = "https://mytest.com/auth"; // Endpoint of the authentication service
        options.TokenValidationParameters = new TokenValidationParameters {
            ValidateAudience = false
        };
    });

// Ensure that the claim type is verified as well
services.AddAuthorization(options => options.AddPolicy("ClientIdPolicy", policy =>
    policy.RequireClaim("client_id", "b778a2ad-090d-4525-8954-6411de2cd339", "abb9c89c-a018-4b0f-9a0f-4e701c637665")));

我遇到的问题是这一直失败。在尝试调试问题之后,我意识到我并不真正理解这样做的目的。是否验证 JWT 上的字段以确保它们有效?如果是这样,我应该为 Authority 提供什么值?我还需要设置其他字段吗?

更新:

经过进一步调查,我发现请求 return 的 WWW-Authenticate 响应 header 包含 Bearer error="invalid_token", error_description="The signature key was not found"。看来我错误地配置了我的身份验证服务或我的下游服务,但我不确定是哪一个。

感谢 article provided by @MichalTrojanowski as well as this post,我能够确定我验证 JWT 的方式存在两个问题:

  1. 我的权限设置有误。或者更确切地说,我的 Authority 值与授权令牌的实际端点匹配,但该值与 /.well-known/openid-configuration 中打印的值不匹配。因此,认证失败。
  2. 我的颁发者与 JWT 中的 iss 值不匹配。

解决这两个问题后,我的服务已经可以正常认证了。