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
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.