'How to properly setup JwtBearerOptions

I setup Identity Server 4 to issue JWT tokens to authenticate users. In Identity Server 4 I have setup the following:

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 = "[email protected]"
                }
            });

            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);
    }
}

and in the Startup.cs of all of my services I have the following:

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")));

The problem I'm having is that this consistently fails. After trying to debug the issue, I've come to the realization that I don't really understand the purpose of this. Is it validating the fields on the JWT to ensure they're valid? If so, what value should I provide for Authority? Are there any other fields I need to set?

Update:

Upon further investigation, I see that requests return with a WWW-Authenticate response header that contains Bearer error="invalid_token", error_description="The signature key was not found". It appears that I've misconfigured either my Authentication service or my downstream services but I'm not sure which.



Solution 1:[1]

Thanks to the article provided by @MichalTrojanowski as well as this post, I was able to determine that there were two problems with how I was authenticating JWTs:

  1. I had my authority set to the wrong value. Or rather, my value for Authority matched the actual endpoint for authorizing tokens but that value did not match what was printed in /.well-known/openid-configuration. Therefore, the authentication failed.
  2. My issuer did not match the iss value in the JWT.

After fixing these two problems, my services have been able to authenticate properly.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Woody1193