I have a NET 6 api that I am using HotChocolate 12.7 in. My queries work, but when I am trying to add the [Authorize] decorator to a query, and send the request with the Bearer token, I am getting the unauthorized response. I can not get it to recognize a properly authenticated user's JWT.
Here is the Program.cs
using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Queries;
using altitude_api.Services;
using altitude_api.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Serilog;
namespace altitude_api
{
public class Program
{
public static void Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var key = configuration.GetSection("AppSettings").GetSection("SecretKey");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
opt.SaveToken = true;
opt.RequireHttpsMetadata = false;
opt.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes((key.Value))),
};
});
builder.Services
.AddGraphQLServer()
.AddAuthorization()
.AddQueryType<Query>()
.AddTypeExtension<HealthQuery>();
var services = builder.Services;
// Add Services
var appSettingsSection = builder.Configuration.GetSection("AppSettings").Get<AppSettings>();
services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
services.AddScoped<IAuthService, AuthService>();
var signinKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(key.Value));
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
Log.Information("App starting up");
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build();
});
var allowedOrigins = appSettingsSection.AllowedOriginList;
services.AddDbContext<AltitudeContext>(
options => options.UseSqlServer(configuration.GetConnectionString("AltitudeContext")));
services.AddCors(options => options.AddPolicy("EnableCORS", build =>
{
build
.AllowAnyMethod()
.AllowAnyHeader()
.WithOrigins(allowedOrigins.ToString())
.AllowCredentials();
}));
var app = builder.Build();
// app.MapControllers();
app.UseRouting();
app.UseCors("EnableCORS");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();
}
}
}
And here is the authentication code.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Novell.Directory.Ldap;
using Serilog;
namespace altitude_api.Services;
public class AuthService: IAuthService
{
private readonly AltitudeContext _altitudeContext;
private readonly AppSettings appSettings;
public AuthService(AltitudeContext altitudeContext, IOptions<AppSettings> _appSettings)
{
_altitudeContext = altitudeContext;
appSettings = _appSettings.Value;
}
public UserWithToken AuthenticateUser(string userName, string password)
{
var isValid = false;
var port = appSettings.LdapPort;
Log.Information($"Beginning to authenticate user {userName}");
using (var cn = new LdapConnection())
{
// connect
try
{
cn.Connect(appSettings.LdapServer, Convert.ToInt32(port));
cn.Bind(appSettings.ldapDomain + userName, password);
if (cn.Bound)
{
isValid = true;
Log.Information($"Successfully found user in LDAP {userName}");
}
}
catch (Exception ex)
{
Log.Error( ex,$"Error looking up {userName} in LDAP.");
throw ex;
}
}
return isValid ? GetToken(userName) : throw new Exception("Unable to authenticate user at this time");
}
public Users GetUser()
{
return _altitudeContext.Users.First(p => p.Username == "maxwell.sands");
}
public UserWithToken GetToken(string userName)
{
try
{
var roles = _altitudeContext.Roles;
var dbUser = _altitudeContext.Users.FirstOrDefault(p => p != null && p.Username == userName);
if (dbUser == null)
{
var ex = new Exception("User not found");
Log.Error(ex, "User is not found could not authenticate");
throw ex;
}
if(dbUser.ExpiryDttm < DateTime.Now)
{
var ex = new Exception("ERROR: User access expired.");
Log.Error(ex, "User is expired could not authenticate");
throw ex;
}
var role = (from rle in _altitudeContext.Roles
join m in _altitudeContext.UserRoles on rle.RoleId equals m.RoleId
join usr in _altitudeContext.Users on m.UserId equals usr.UserId
where usr.Username.ToLower() == dbUser.Username.ToLower()
select rle.RoleName).FirstOrDefault();
if (role == null)
{
var ex = new Exception("Role not found");
Log.Error(ex, "User is expired could not authenticate");
throw ex;
}
var secret = appSettings.SecretKey;
// authentication successful so generate jwt token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, role),
}),
Expires = DateTime.UtcNow.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
UserWithToken webuser = new UserWithToken();
webuser.UserName = dbUser.Username;
webuser.FirstName = dbUser.FirstName;
webuser.LastName = dbUser.LastName;
webuser.Id = dbUser.UserId;
webuser.Role = role;
webuser.Token = tokenHandler.WriteToken(token);
Log.Information($"{webuser.FirstName} {webuser.LastName} was successfully logged in.");
return webuser;
}
catch(Exception e)
{
Log.Information(e, "There was an error loading the user");
throw new Exception("There was an issue loading the user", e);
}
}
}
And finally here is the endpoint I am require authorization on.
using HotChocolate.AspNetCore.Authorization;
namespace altitude_api.Queries;
[ExtendObjectType(typeof(Query))]
public class HealthQuery
{
[Authorize]
public bool HeartBeat()
{
return true;
}
}
In response to #Arjav Dave, yes I did find the answer to this, almost by dumb luck. The order of things in the Program.cs is very important. My code looks like the following...
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
// omitted
}).Services
.AddAuthorization();
builder.Services
.AddGraphQLServer()
.AddAuthorization()
.AddFiltering()
.AddSorting()
.AddQueryType<Query>()
and when I am setting up the app it looks like...
var app = builder.Build();
app.UseRouting();
app.UseCors("EnableCORS");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();
Hope that helps.
Related
I have an issue where no matter which settings I adjust, a user who has signed in is forcibly logged out after 30 minutes. I would like to extend this timeframe to 6 hours.
I am using a system where login credentials are provided externally. The system receives a JWT and uses the information contained there to authenticate the user. This is the root cause of the issue; users that log in via the .NET Framework's built-in authentication system don't have this problem. I don't know why or how.
Here are the highlights of what I have tried:
configuring application cookies in Program.cs:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.ExpireTimeSpan = TimeSpan.FromHours(6);
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
});
Using persistence and authentication properties in the authentication process:
await _signInManager.SignInAsync(
user,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6)
});
I have ensured I have data protection enabled in Program.cs:
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
I have published it to a dedicated IIS server where I set the Idle Timeout to 6 hours.
I have set SlidingExpiration = true in all the places that have the option to do so.
I have tried all of the above together, separate, and in combination. User sessions are still restricted to 30 minutes. When I set the Application Cookies to expire after 1 second, the session expires after 1 second. But when I set them to 6 hours, it still expires at 30 minutes. I have no idea why.
What am I missing here? I have been struggling with this for days and have still found no solution. I've found similar questions on Stack Overflow and their solutions all include what I have tried here, to no avail.
I have double-checked and noticed that the 30 minute time-out does not affect users that authenticate via the .NET Framework's built-in login system (where they enter their username/password). This only seems to affect users that authenticate using the credentials delivered via JWT.
Here is the full Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("ConnectionString");
builder.Services.AddDbContext<DBContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddControllersWithViews();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(6);
});
//Add JWT bearer and denied paths
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Jwt:Key"))
};
});
//GDPR compliance
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
builder.Services.ConfigureNonBreakingSameSiteCookies();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = x =>
{
x.Response.Redirect("https://localhost:44329/Expired/Index/");
return Task.CompletedTask;
}
};
options.ExpireTimeSpan = TimeSpan.FromDays(14);
});
//define policy for different authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Administrator"));
options.AddPolicy("UsersOnly", policy => policy.RequireRole("User", "Editor", "Author"));
options.AddPolicy("RequireApprovedUser", policy => policy.Requirements.Add(new ApprovedUserRequirement(true)));
});
builder.Services.AddScoped<IAuthorizationHandler, ApprovedUserRequirementHandler>();
//Data Protection configuration
var keysFolder = Path.Combine(builder.Environment.ContentRootPath, "UserKeys");
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<DBContext>();
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(6);
options.Lockout.MaxFailedAccessAttempts = 5;
options.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultTokenProviders()
.AddDefaultUI()
.AddEntityFrameworkStores<DBContext>();
builder.Services.AddRazorPages();
//this will get the email settings in appsettings
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
//Use for sending email
builder.Services.AddTransient<IMailService, Project.Services.EmailSender>();
// Register the Google Analytics configuration
builder.Services.Configure<GoogleAnalyticsOptions>(options => builder.Configuration.GetSection("GoogleAnalytics").Bind(options));
// Register the TagHelperComponent for Google Analytics
builder.Services.AddTransient<ITagHelperComponent, GoogleAnalyticsTagHelper>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, UserClaimsPrincipalFactory<IdentityUser, IdentityRole>>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode >= 400)
{
context.Request.Path = "/Error/Index/" + context.Response.StatusCode;
await next();
}
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(builder => builder.AllowAnyOrigin()
.WithOrigins("https://localhost:44329", "https://localhost:44329")
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
Here is the code that authenticates incoming users:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}
Here are the settings I used in IIS. Note that I will have trouble adjusting settings unavailable on Plesk.
Welp, it took a lot of digging, but I found the solution.
I needed to ensure that users were saved to the IdentityUser tables using _userManager.CreateAsync(). The code for registering users I used would create a new IdentityUser every time without ensuring it went in the database. Without an entry in the IdentityUser tables, .NET Core can't use keys or sessions properly.
Here is the change to that code:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser? iUser;
//check to see if user is in IdentityUser
iUser = await _userManager.FindByEmailAsync(user.Email);
//if user is not in IdentityUser, create a new one:
if(iUser == null)
{
iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
//Very important! This allows the system to remember users
//Performing this operation also gives iUser the necessary keys
await _userManager.CreateAsync(iUser);
}
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}
I upgraded my app from .NET Core 2.2 to .NET Core 3.1. When I tried to test out my endpoints with PostMan I noticed I was getting 401 Unauth Error. When I look at the header I am seeing that the expiry time is invalid:
I took the following bearer token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiQm9iIiwibmJmIjoiMTYxNzk3Nzg1MSIsImV4cCI6IjE2MjMxNjE4NTYiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiQmFza2V0YmFsbCIsIlJ1Z2J5IiwiRm9vdGJhbGwiXX0.QRLuXFeopf7QZ1NUzWcctuSfnNXiPgc2UH7NxAuHYvw
that I am getting on my generate token endpoint and decoded it with jwt.io and the exp field is "1623161856".
I converted that to a date object in Javascript and it equals 60 days in the future.
So token certainly is not expired. Not sure if I missing anything in the upgrade to .NET Core 3.1 but here is the relevant code:
In Startup.cs I have
public void ConfigureServices(IServiceCollection services)
{
// Initial Setup
services.AddMvc();
services.AddSingleton<IConfiguration>(Configuration);
// Call this in case you need aspnet-user-authtype/aspnet-user-identity
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Register the Swagger generator, defining one or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc(Configuration["v1"], new OpenApiInfo { Title = Configuration["Sports"], Version = Configuration["v1] });
});
services.AddDataProtection();
//Authentication Setup
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("AnythingYouWant")),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
options.SaveToken = true;
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
var accessToken = context.SecurityToken as JwtSecurityToken;
if (accessToken != null)
{
ClaimsIdentity identity = context.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
identity.AddClaim(new Claim("access_token", accessToken.RawData));
}
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/" + Configuration["v1"] + "/swagger.json", Configuration["Sports"]);
});
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("swagger", "swagger/");
});
app.UseWelcomePage("/swagger");
}
And the token is generated by one of my api endpoints. That code is as shown:
[HttpPost("SportApi/Token")]
[ServiceFilter(typeof(SportResourceFilter))]
public IActionResult Create(string key)
{
return new ObjectResult(GenerateToken(key));
}
private string GenerateToken(string someKey)
{
JwtSecurityToken token = new JwtSecurityToken();
List<SportAPIKey> ro = new List<SportAPIKey>();
if (!string.IsNullOrEmpty(someKey))
{
using (StreamReader r = new StreamReader("keys.json"))
{
string json = r.ReadToEnd();
ro = JsonConvert.DeserializeObject<List<SportAPIKey>>(json);
}
if (ro.Exists(sak => sak.SportAPIKeyValue.Equals(someKey)))
{
SportAPIKey sportapikey = ro.Find(sak => sak.SportAPIKeyValue.Equals(someKey));
List<Claim> lc = new List<Claim>();
Claim claimClient = new Claim(ClaimTypes.Name, sportapikey.Client);
lc.Add(claimClient);
foreach (string team in sportapikey.Teams)
{
lc.Add(new Claim(ClaimTypes.System, team.Trim()));
}
Claim claimEffDate = new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString());
lc.Add(claimEffDate);
int tokenLifespan = 60;
Claim claimExpDate = new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString());
lc.Add(claimExpDate);
foreach (string sport in sportapikey.Sports.Split(","))
{
lc.Add(new Claim(ClaimTypes.Role, sport.Trim()));
}
var claims = lc.ToArray();
token = new JwtSecurityToken(
new JwtHeader(new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("AnythingYouWant")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
}
}
return new JwtSecurityTokenHandler().WriteToken(token);
}
Your token contains the timestamps nbfand expas string:
"nbf": "1617977851",
"exp": "1623161856",
which is invalid. On https:\jwt.io you can see that something is wrong when you hover with your mouse over these values. Ususally it shows the timestamp, but in your example it shows: "Invalid date":
Timestamps in a JWT are supposed to be numerical values:
NumericDate
A JSON numeric value representing the number of seconds from
1970-01-01T00:00:00Z UTC until the specified UTC date/time
The values itself are correct:
The error happens during generation of the claims:
Claim claimExpDate = new Claim(JwtRegisteredClaimNames.Exp,
new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString());
lc.Add(claimExpDate);
Instead use this constructor, which lets you set the ClaimValueType:
new Claim(JwtRegisteredClaimNames.Exp,
new DateTimeOffset(DateTime.Now.AddDays(tokenLifespan)).ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
Or even better, let the framework add the expiration timestamp for you, using this constructor:
public JwtSecurityToken (string issuer = default, string audience = default, System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> claims = default, DateTime? notBefore = default, DateTime? expires = default, Microsoft.IdentityModel.Tokens.SigningCredentials signingCredentials = default);
e.g.:
JwtSecurityToken(expires: DateTime.UtcNow.AddDays(60), ...)
We are in the middle of upgrading our ASP.NET Core 1.0 web app to ASP.NET Core 3.1.
One thing that isn't working for us right now is our export service, which is used to generate PDFs from cshtml files. This code used to work on ASP.NET Core 1 app. Here's the code for rendering the cshtml files into a string:
public async Task<string> Export(object viewModel, string reportName, eExportType exportType, PdfExportOptions options)
{
using (var writer = new StringWriter())
{
var viewEngine = _context.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
// Find the view
var viewResult = viewEngine.GetView($"~/Views/Reports/{reportName}.cshtml", $"~/Views/Reports/{reportName}.cshtml", false);
if (viewResult?.Success == false) // This is the line that's not working: Success equals false
{
throw new Exception($"The report {reportName} could not be found.");
}
// Build ViewContext
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = viewModel
};
var tempData = new TempDataDictionary(_context, new SessionStateTempDataProvider(null));
var routingFeature = _context.Features[typeof(IRoutingFeature)] as RoutingFeature;
var actionContext = new ActionContext(_context, routingFeature?.RouteData, new ActionDescriptor());
var viewContext = new ViewContext(actionContext, viewResult?.View, viewData, tempData, writer, new HtmlHelperOptions());
// Render the view
if (viewResult?.View != null)
await viewResult.View?.RenderAsync(viewContext);
// Invoke the node.js service
var htmlContent = writer.GetStringBuilder().ToString();
var nodeServiceName = $"./NodeServices/{exportType.ToString().ToLower()}";
//var exportHandler = GetExportHandler(exportType, options); // TODO: Currently only works with PDF. Use export handlers for different export types
var result = await _nodeServices.InvokeAsync<byte[]>(nodeServiceName, htmlContent, options);
return Convert.ToBase64String(result);
}
}
For some reason viewResult?.Success is always false. I've tried all kinds of combinations to make it work, like viewEngine.GetView($"~/Views/Reports/{reportName}.cshtml", $"~/Views/Reports/{reportName}.cshtml", false),
viewEngine.GetView($"~/Views/Reports/{reportName}.cshtml", $"{reportName}.cshtml", false),
viewEngine.GetView($"Views/Reports/{reportName}.cshtml", $"{reportName}.cshtml", false)
etc. but none of them works, Success is always false, and the proerty viewResult.View is always null.
I've looked at many posts on StackOverflow, like this and many others but none of them solves our problem.
I'm suspecting that we might be doing something wrong at our Startup class, because the variable routingFeature above is also null, but I'm not completely sure about that. But just to make sure that out Startup class is configured correctly, here's the code for it:
public class Startup
{
public Startup(/*IHostingEnvironment env,*/ IConfiguration configuration)
{
Configuration = (IConfigurationRoot)configuration;
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.Configure<SmsProvidersSettings>(Configuration.GetSection("SmsProvidersSettings"));
// More configuration sections here, removed for brevity
services.AddControllers().AddNewtonsoftJson(options =>
{
// Use the default property (Pascal) casing
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization();
var sessionTimeout = Convert.ToDouble(Configuration.GetSection("GlobalSettings")["SessionTimeout"]);
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new AssemblyPart(typeof(Startup).Assembly));
services.AddSingleton(manager);
services.AddDistributedMemoryCache();
services.AddSession(options => options.IdleTimeout = TimeSpan.FromSeconds(sessionTimeout));
////services.AddMvc(options =>
////{
//// options.Filters.Add(new UserActionAuditFilterAttribute(new AuditHandler(Configuration["ConnectionStrings:Audit"])));
//// options.Filters.Add(new ApiValidationFilterAttribute());
//// options.Filters.Add(new GlobalExceptionFilter());
////});
//services.AddMemoryCache();
services.AddNodeServices(options => options.InvocationTimeoutMilliseconds = 60000);
services.Configure<RequestLocalizationOptions>(opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("he-IL")
};
opts.DefaultRequestCulture = new RequestCulture("he-IL", "he-IL");
opts.SupportedCultures = supportedCultures;
opts.SupportedUICultures = supportedCultures;
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
if (Convert.ToBoolean(Configuration.GetSection("GlobalSettings")["UseScheduler"]))
{
services.AddHangfire(configuration =>
configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(
Configuration["ConnectionStrings:DigitalRural"],
new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
UseRecommendedIsolationLevel = true,
PrepareSchemaIfNecessary = true, // Default value: true
EnableHeavyMigrations = true, // Default value: false
UsePageLocksOnDequeue = true,
DisableGlobalLocks = false
}));
}
services.AddTransient<IRecurringJobManager, RecurringJobManager>();
services.AddTransient<IActionSystemService, ActionSystemService>();
// More serivces here, removed for brevity
services.AddScoped<IUnitOfWork>(sp =>
{
var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
var currentUser = httpContextAccessor.HttpContext?.Session.GetJson<SessionUser>(SessionStateKeys.CurrentUser);
int? userId = null;
if (currentUser != null)
userId = currentUser.UserId;
return new UnitOfWork(new DigitalRuralContext(Configuration["ConnectionStrings:DigitalRural"], userId), httpContextAccessor);
});
//
services.AddControllersWithViews();
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime, IGlobalApplicationDataService applicationDataService)
{
//var applicationDataService = app.ApplicationServices.GetService(typeof(IGlobalApplicationDataService)) as IGlobalApplicationDataService;
applicationDataService?.GetApplicationDataVM();
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
// Don't cache index.html
context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
context.Response.Headers.Add("Pragma", "no-cache");
}
await next();
// If there's no available file and the request doesn't contain an extension, we're probably trying to access a page.
// Rewrite request to use app root
if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value))
{
context.Request.Path = "/"; // Put your Angular root page here
await next();
}
});
app.UseAuthentication();
app.UseAuthorization();
loggerFactory
.AddSerilog();
//// Ensure any buffered events are sent at shutdown
//appLifetime.ApplicationStopped.Register(Log.CloseAndFlush);
//if (env.IsDevelopment())
//{
// app.UseDeveloperExceptionPage();
// app.UseBrowserLink();
//}
//else
//{
// app.UseExceptionHandler("/Home/Error");
//}
////app.UseDefaultFiles();
//app.UseStaticFiles(new StaticFileOptions
//{
// OnPrepareResponse = context => { }
//});
////app.UseMvc();
if (Convert.ToBoolean(Configuration.GetSection("GlobalSettings")["UseScheduler"]))
{
app.UseHangfireDashboard("/scheduler", new DashboardOptions // Will be available at http://localhost:60209/scheduler
{
Authorization = new[] { new HangfireDashbordAuthorizationFilter() }
});
app.UseHangfireServer(new BackgroundJobServerOptions { StopTimeout = TimeSpan.FromSeconds(10) });
}
var applicationServices = app.ApplicationServices;
var httpContextAccessor = applicationServices.GetService<IHttpContextAccessor>();
app.UseFileServer(new FileServerOptions { FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")), RequestPath = "", EnableDefaultFiles = true });
app.UseRouting();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
}
}
So any guess as to why our code doesn't work?
I am getting 403 Forbidden while trying to implement Role based JWT Authorization using .NET Core Web API 3.1 version. Below is how my code looks like:
// API
[HttpGet, Route("GetAll")]
[Authorize(Roles = "Admin")]
public IEnumerable<Users> GetAllUsers(string environment) {}
// JWT Token Generation
public UserDetail GenerateToken(string userName, string password)
{
string key = Configuration["Jwt:Key"];
var issuer = Configuration["Jwt:Issuer"];
var audience = Configuration["Jwt:Audience"];
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
IdentityModelEventSource.ShowPII = true;
//Create a List of Claims, Keep claims name short
var roleClaims = GetRoleClaimsFor(user);
//Create Security Token object by giving required parameters
var token = new JwtSecurityToken(issuer,
audience: audience,
claims: roleClaims,
expires: DateTime.Now.AddMinutes(15),
signingCredentials: credentials);
//Generate JWT Token
var userToken = new JwtSecurityTokenHandler().WriteToken(token);
}
// StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = Configuration["Jwt:Policy"];
}).AddJwtBearer(Configuration["Jwt:Policy"], options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
// JWT Authorization configuration
services.AddAuthorization(auth =>
{
auth.AddPolicy(Configuration["Jwt:Policy"], new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireClaim(ClaimTypes.Name, Configuration["Jwt:RequiredClaim"]).Build());
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
}
private IList<Claim> GetRoleClaimsFor(UserPrincipal user)
{
var roleClaims = new List<Claim>();
UserRoles = new List<string>();
// Pull only groups where user belongs to
PrincipalSearchResult<Principal> groups = user.GetGroups();
// Filter user groups related to GBITs application alone
var gbitsGroups = (from grp in groups
select grp);
// Loop through gbitsGroups and assign the related role
foreach (GroupPrincipal gbitsGrp in gbitsGroups)
{
try
{
// Pull UserGroup related Permissions
var roles = _userGroupPermissionRepository.GetUserGroupPermissionFor(gbitsGrp.Name);
// Loop through UserGroupPermissions to create Claims
foreach (var role in roles)
{
var claim = new Claim(role.UserGroup.GroupName, role.Permission.PermissionName);
roleClaims.Add(claim);
}
// Load Roles collection
IList<string> usrRoles = (from role in roles
select role.Permission.PermissionName).ToList();
UserRoles.AddRange(usrRoles);
}
catch (Exception ex)
{
throw ex;
}
}
return roleClaims;
}
I am able to run and see all the roles as claims from Principal.Identity after validating the token. But the Roles are not working as expected.
Creating the claims as ClaimsType.Role instead of custom string solved the issue. Below is the refactored GetClaimsFor method
private IList<Claim> GetRoleClaimsFor(UserPrincipal user)
{
var roleClaims = new List<Claim>();
UserRoles = new List<string>();
// Pull only groups where user belongs to
PrincipalSearchResult<Principal> groups = user.GetGroups();
// Filter user groups related to GBITs application alone
var gbitsGroups = (from grp in groups
select grp);
// Loop through gbitsGroups and assign the related role
foreach (GroupPrincipal gbitsGrp in gbitsGroups)
{
try
{
// Pull UserGroup related Permissions
var roles = _userGroupPermissionRepository.GetUserGroupPermissionFor(gbitsGrp.Name);
// Loop through UserGroupPermissions to create Claims
foreach (var role in roles)
{
var claim = new Claim(ClaimsType.Role, role.Permission.PermissionName);
roleClaims.Add(claim);
}
// Load Roles collection
IList<string> usrRoles = (from role in roles
select role.Permission.PermissionName).ToList();
UserRoles.AddRange(usrRoles);
}
catch (Exception ex)
{
throw ex;
}
}
return roleClaims;
}
Having used a refresh token to get a new access token, I want to update my client side cookie with that access token.
My client is able to sign in and call my REST API using ajax, however when that first authorization expires, naturally the API calls no longer work.
I have a .NET web application which consumes its own REST API. The API is a part of the same project. It does not have its own startup configuration.
As the cookie is being sent in the header of each request it needs to have the new unexpired access token so that I don't get 'User unauthorized' for the request.
Right now I am able to get a new token using my refresh token but the value of the cookie has not changed, so I believe I need to update my cookie to reflect the new access token before the client sends any requests.
Here's a look at my hybrid client:
using IdentityModel.Client;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Cts.HomeService.Web.App_Start
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
var identityServerSection = (IdentityServerSectionHandler)System.Configuration.ConfigurationManager.GetSection("identityserversection");
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "localTestClient",
Authority = "http://localhost:5000",
RedirectUri = identityServerSection.Identity.RedirectUri,
Scope = "openid profile offline_access",
ResponseType = "code id_token",
RequireHttpsMetadata = false,
PostLogoutRedirectUri = identityServerSection.Identity.RedirectUri,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
var tokenClient = new TokenClient(
"http://localhost:5000/connect/token",
"localTestClient",
"");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
"http://localhost:5000/connect/userinfo");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", tokenResponse.IdentityToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
{
// so here I'll grab the access token
if (isAccessTokenExpired()) {
var cancellationToken = new CancellationToken();
var newAccessToken = context.GetNewAccessTokenAsync(refresh_token, null, cancellationToken);
// now what?
}
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
}
});
}
}
}
I've looked into a lot of things but the value of my cookie always stays the same. I've considered deleting the old cookie and just building the new cookie manually, but that requires encrypting it the right way and it smells funny, surely not the idiomatic way to do it.
I feel there must be something simple I am missing. I would expect a simple "UpdateCookie(newToken)" kind of method and I have tried SignIn() and SignOut() but these have not worked out for me, seemingly not interacting with the cookie at all in fact.
This is how I got mine to work, add the following lines:
SecurityTokenValidated = context =>
{
context.AuthenticationTicket.Properties.AllowRefresh = true;
context.AuthenticationTicket.Properties.IsPersistent = true;
}
Then in AuthorizationCodeReceived add this to the end:
HttpContext.Current.GetOwinContext().Authentication.SignIn(new AuthenticationProperties
{
ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn),
AllowRefresh = true,
IssuedUtc = DateTime.UtcNow,
IsPersistent = true
}, newIdentity);
Where newIdentity is your claims identity, hope this helps.
I recently stuck with the same question and the solution is:
Set UseTokenLifetime = false in OpenIdConnectAuthenticationOptions used to configure OAuth middleware (otherwise session cookie lifetime will be set to access token lifetime, which is usually one hour)
Create your own CookieAuthenticationProvider that validates access token expiration
When token is expired (or close to be expired):
Get new access token using refresh token (if MSAL is used for OAuth - this is a simple IConfidentialClientApplication.AcquireTokenSilent() method call)
Build a fresh IIdentity object with the acquired access token using ISecurityTokenValidator.ValidateToken() method
Replace request context identity by the newly built identity
Call IAuthenticationManager.SignIn(properties, freshIdentity) to update the session cookie
Here is the full solution to make refresh tokens work with OWIN cookie middleware:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EPiServer.Logging;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Host.SystemWeb;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
namespace MyApp
{
public class OwinStartup
{
public void Configuration(IAppBuilder app)
{
var openIdConnectOptions = new OpenIdConnectAuthenticationOptions
{
UseTokenLifetime = false,
// ...
};
var msalAppBuilder = new MsalAppBuilder();
var refreshTokenHandler = new RefreshTokenHandler(msalAppBuilder, openIdConnectOptions);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebChunkingCookieManager(),
Provider = new RefreshTokenCookieAuthenticationProvider(refreshTokenHandler)
});
}
}
public class RefreshTokenCookieAuthenticationProvider : CookieAuthenticationProvider
{
private readonly RefreshTokenHandler _refreshTokenHandler;
private static readonly ILogger _log = LogManager.GetLogger();
public RefreshTokenCookieAuthenticationProvider(RefreshTokenHandler refreshTokenHandler)
{
_refreshTokenHandler = refreshTokenHandler;
}
public override async Task ValidateIdentity(CookieValidateIdentityContext context)
{
var exp = context.Identity?.FindFirst("exp")?.Value;
if (string.IsNullOrEmpty(exp))
{
return;
}
var utcNow = DateTimeOffset.UtcNow;
var expiresUtc = DateTimeOffset.FromUnixTimeSeconds(long.Parse(exp));
var maxMinsBeforeExpires = TimeSpan.FromMinutes(2);
if (expiresUtc - utcNow >= maxMinsBeforeExpires)
{
return;
}
try
{
var freshIdentity = await _refreshTokenHandler.TryRefreshAccessTokenAsync(context.Identity);
if (freshIdentity != null)
{
context.ReplaceIdentity(freshIdentity);
context.OwinContext.Authentication.SignIn(context.Properties, (ClaimsIdentity) freshIdentity);
}
else
{
context.RejectIdentity();
}
}
catch (Exception ex)
{
_log.Error("Can't refresh user token", ex);
context.RejectIdentity();
}
}
}
public class RefreshTokenHandler
{
private readonly MsalAppBuilder _msalAppBuilder;
private readonly OpenIdConnectAuthenticationOptions _openIdConnectOptions;
public RefreshTokenHandler(
MsalAppBuilder msalAppBuilder,
OpenIdConnectAuthenticationOptions openIdConnectOptions)
{
_msalAppBuilder = msalAppBuilder;
_openIdConnectOptions = openIdConnectOptions;
}
public async Task<IIdentity> TryRefreshAccessTokenAsync(IIdentity identity, CancellationToken ct = default)
{
try
{
var idToken = await GetFreshIdTokenAsync(identity, ct);
var freshIdentity = await GetFreshIdentityAsync(idToken, ct);
return freshIdentity;
}
catch (MsalUiRequiredException)
{
return null;
}
}
private async Task<string> GetFreshIdTokenAsync(IIdentity identity, CancellationToken ct)
{
var principal = new ClaimsPrincipal(identity);
var app = _msalAppBuilder.BuildConfidentialClientApplication(principal);
var accounts = await app.GetAccountsAsync();
var result = await app.AcquireTokenSilent(new[] {"openid"}, accounts.FirstOrDefault()).ExecuteAsync(ct);
return result.IdToken;
}
private async Task<IIdentity> GetFreshIdentityAsync(string idToken, CancellationToken ct)
{
var validationParameters = await CreateTokenValidationParametersAsync(ct);
var principal = _openIdConnectOptions.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out _);
var identity = (ClaimsIdentity) principal.Identity;
return identity;
}
// This is additional code for cases with multiple issuers - can be skipped if this configuration is static
private async Task<TokenValidationParameters> CreateTokenValidationParametersAsync(CancellationToken ct)
{
var validationParameters = _openIdConnectOptions.TokenValidationParameters.Clone();
var configuration = await _openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(ct);
validationParameters.ValidIssuers = (validationParameters.ValidIssuers ?? new string[0])
.Union(new[] {configuration.Issuer})
.ToList();
validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys ?? new SecurityKey[0])
.Union(configuration.SigningKeys)
.ToList();
return validationParameters;
}
}
// From official samples: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/blob/master/TaskWebApp/Utils/MsalAppBuilder.cs
public class MsalAppBuilder
{
public IConfidentialClientApplication BuildConfidentialClientApplication(ClaimsPrincipal currentUser)
{
// ...
}
}
}