Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 857
Mofid.Moghimi Avatar asked Nov 07 '25 11:11

Mofid.Moghimi


1 Answers

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.

like image 100
Aquiles Avatar answered Nov 09 '25 00:11

Aquiles



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!