Search code examples
c#jwtasp.net-core-3.1jwe

How to set Dynamic IssuerSigningKey and TokenDecryptionKey in AddJwtBearer options in ConfigureServices method on Startup.cs file


I wrote an Authentication web api project with DotNet core 3.1 and Microsoft.IdentityModel.JsonWebTokens.

I have 3 client app-android, app-pwa and admin-panel. each client has specific SigningCredentials and EncryptingCredentials key on database.

When I want to generate access token, I use one of these client.

var token = new SecurityTokenDescriptor()
        {
            Audience = client.Audience,
            Claims = claims,
            Expires = DateTimeOffset.UtcNow.Add(client.AccessTokenLifeTime).DateTime,
            Issuer = client.Issuer.GetDisplayName(),
            CompressionAlgorithm = client.SupportCompression ? CompressionAlgorithms.Deflate : null,
            IssuedAt = DateTime.UtcNow,
            NotBefore = DateTime.UtcNow,
            EncryptingCredentials =
                new EncryptingCredentials(
                    new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(client.EncryptingKey)),
                    JwtConstants.DirectKeyUseAlg, SecurityAlgorithms.Aes256CbcHmacSha512),
            SigningCredentials =
                new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(client.SigningKey)),
                    SecurityAlgorithms.HmacSha256Signature),
        };

In Startup.cs file i need to set AddJwtBearer options

AddJwtBearer(x => x.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            TokenDecryptionKey =
                new SymmetricSecurityKey(
                    Encoding.ASCII.GetBytes("encrypt_key")),
             IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("security_key"))
        })

But I need to set these keys dynamicly per client

How can I do this?

thanks


Solution

  • I am working with Net Core 5, and I implemented some code that can guide you. Please be patient, because this example has a lot of code.

    I stored at database the TokenValidationParameter information in the ApplicationCompany table, in my example:

    [Index(nameof(AuthenticationScheme), IsUnique = true)]
    [Table("application_company")]
    public class ApplicationCompany
    {
        [Key, Column("id")]
        public int Id { get; set; }
    
        [Column("authentication_schema")]
        public string AuthenticationScheme { get; set; }
    
        [Column("company_id"), ForeignKey("Company")]
        public int CompanyId { get; set; }
    
        [Column("application_id"), ForeignKey("Application")]
        public int ApplicationId { get; set; }
    
        [Column("environment_id"), ForeignKey("Environment")]
        public int EnvironmentId { get; set; }
    
        [Column("valid_audience")]
        public string ValidAudience { get; set; }
    
        [Column("valid_issuer")]
        public string ValidIssuer { get; set; }
    
        [Column("access_token_secret")]
        public string AccessTokenSecret { get; set; }
    
        [Column("refresh_token_secret")]
        public string RefreshTokenSecret { get; set; }
    
        [Column("access_token_expiration")]
        public int AccessTokenExpirationMinutes { get; set; }
    
        [Column("refresh_token_expiration")]
        public int RefreshTokenExpirationMinutes { get; set; }
    
        public virtual Company Company { get; set; }
    
        public virtual Application Application { get; set; }
    
        public virtual Environment Environment { get; set; }
    }
    

    Please remember it is not the purpose of this post to explain my database shema, neither how I implemented Unit Of Work and DBContext management.

    Then, in the Startup.cs, at ConfigureServices() I obtained all my configured information at applicationCompanies variable. After that, I configured "MyScheme" scheme, with my default options.

       public void ConfigureServices(IServiceCollection services)
        {
            ...    
    
            // If you don't register IHttpContextAccessor explicitly before GetConfiguredApplications(), your DbContext class 
            // will have optionsBuilder.IsConfigured = false at OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            // Reference: https://stackoverflow.com/questions/38338475/no-database-provider-has-been-configured-for-this-dbcontext-on-signinmanager-p
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
            // Get ApplicationCompany list from Postgres database
            var applicationCompanies = GetConfiguredApplications(services);
    
            // Add Jwt Bearer
            var authBuilder = services.AddAuthentication(options =>
            {
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer("MyScheme", options =>
            {
                var key = Encoding.UTF8.GetBytes(Configuration["JwtConfig:AccessTokenSecret"]);
    
                options.SaveToken = true;
                options.RequireHttpsMetadata = true;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true, // this will validate the 3rd part of the jwt token using the secret that we added in the appsettings and verify we have generated the jwt token
                    IssuerSigningKey = new SymmetricSecurityKey(key), // Add the secret key to our Jwt encryption
                    ValidateIssuer = true, // The issuer of the token
                    ValidateAudience = true, // The audience of the token
                    ValidAudiences = Configuration["JWTConfig:ValidAudience"].Split(";"),
                    ValidIssuer = Configuration["JWTConfig:ValidIssuer"],
                    RequireExpirationTime = true,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                };
            });
    
            ...
    

    After configuring my default JwtBearer processing in the ConfigureServices(), I iterated my applicationCompanies list and added more schemes to my Jwt processing:

            // Add other JWT Schemas
            foreach (var applicationCompany in applicationCompanies)
            {
                authBuilder.
                    AddJwtBearer(applicationCompany.AuthenticationScheme, options =>
                    {
                        var key = Encoding.UTF8.GetBytes(applicationCompany.AccessTokenSecret);
    
                        options.SaveToken = true;
                        options.RequireHttpsMetadata = true; 
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuerSigningKey = true, // this will validate the 3rd part of the jwt token using the secret that we added in the appsettings and verify we have generated the jwt token
                            IssuerSigningKey = new SymmetricSecurityKey(key), // Add the secret key to our Jwt encryption
                            ValidateIssuer = true, // The issuer of the token
                            ValidateAudience = true, // The audience of the token
                            ValidAudiences = applicationCompany.ValidAudience.Split(";"),
                            ValidIssuer = Configuration["JWTConfig:ValidIssuer"],
                            RequireExpirationTime = true,
                            ValidateLifetime = true,
                            ClockSkew = TimeSpan.Zero
                        };
                    });
            }
               // Consider ApplicationCompany modifications
            AddMultiSchemeJwtBearerAuthentication(services, Configuration, authBuilder);
    

    Then, in the AddMultiSchemeJwtBearerAuthentication() I provide a "Selector" policy for the default authentication scheme, where I decide what schema to apply according to the received tokens. And in this same processing, I set dynamically AddJwtBearer options when I use services.PostConfigure.

        private static void AddMultiSchemeJwtBearerAuthentication(IServiceCollection services, IConfiguration Configuration, AuthenticationBuilder authenticationBuilder)
        {
            // Add scheme selector.
            authenticationBuilder.AddPolicyScheme(
                JwtBearerDefaults.AuthenticationScheme,
                "Selector",
                options =>
                {
                    options.ForwardDefaultSelector = context =>
                    {
                        string token = JwtUtils.GetToken(context);
    
                        if (!String.IsNullOrEmpty(token))
                        {
                            var jwtHandler = new JwtSecurityTokenHandler();
    
                            var decodedToken = jwtHandler.ReadJwtToken(token);
    
                            List<Claim> claimList = decodedToken?.Claims?.ToList();
    
                            if (claimList != null)
                            {
                                string scheme = claimList.Find(x => x.Type.Equals("scheme"))?.Value;
    
                                var applicationCompany = Startup.GetConfiguredApplicationByScheme(services, scheme);
    
                                if (applicationCompany != null)
                                {
                                    // In order to update dinamically JWT configuration according to database
                                    services.PostConfigure<JwtBearerOptions>(scheme, options =>
                                    {
                                        var key = Encoding.UTF8.GetBytes(applicationCompany.AccessTokenSecret);
    
                                        options.TokenValidationParameters = new TokenValidationParameters
                                        {
                                            ValidateIssuerSigningKey = true, // this will validate the 3rd part of the jwt token using the secret that we added in the appsettings and verify we have generated the jwt token
                                            IssuerSigningKey = new SymmetricSecurityKey(key), // Add the secret key to our Jwt encryption
                                            ValidateIssuer = true, // The issuer of the token
                                            ValidateAudience = true, // The audience of the token
                                            ValidAudiences = applicationCompany.ValidAudience.Split(";"),
                                            ValidIssuer = Configuration["JWTConfig:ValidIssuer"],
                                            RequireExpirationTime = true,
                                            ValidateLifetime = true,
                                            ClockSkew = TimeSpan.Zero
                                        };
                                    });
                                    return scheme;
                                }
                            }
                        }
                        return "MyScheme";
                    };
                }
            );
        }
    

    In my Login() method I used the generated token to store a claim with the ClaimType "scheme". I used this claim type to decide what scheme to use in the AddMultiSchemeJwtBearerAuthentication() method at Startup.cs.

       authClaims.Add(new Claim("scheme", applicationCompany.AuthenticationScheme));
    

    Also, I give you my way of obtaining the token:

        public static string GetToken(HttpContext context)
        {
            string authHeader = context.Request.Headers["Authorization"];
            string token = null;
    
            if (authHeader != null && authHeader.Contains("Bearer "))
            {
                token = authHeader[7..];
            }
    
            return token;
        }
    

    and the way I used to get my configured ApplicationCompany list from Database:

        private static List<ApplicationCompany> GetConfiguredApplications(IServiceCollection services)
        {
            List<ApplicationCompany> result = new List<ApplicationCompany>();
    
            try
            {
                var sp = services.BuildServiceProvider();
    
                var unitOfWork = sp.GetRequiredService<IUnitOfWork>();
    
                var task = Task.Run(async () => await unitOfWork.ApplicationCompanies.GetAllAsync());
                if (task.IsFaulted && task.Exception != null)
                {
                    throw task.Exception;
                }
                result = task.Result as List<ApplicationCompany>;
            }
            catch (Exception)
            {
                Console.WriteLine("Cannot build Application Company list");
            }
    
            return result;
        }
    

    I hope this help suits you well. And I feel glad on receiving constructive comments.