#If (User.IsInRole ("Admin")) returns true when I login to the site. However, when I buy a new token with reflesh token, #if (User.IsInRole ("Admin")) returns false. I host the token in the cookie.
Startup.cs
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, opts =>
{
var tokenOptions = Configuration.GetSection("TokenOption").Get<CustomTokenOption>();
opts.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidIssuer = tokenOptions.Issuer,
ValidAudience = tokenOptions.Audience[0],
IssuerSigningKey = SignService.GetSymmetricSecurityKey(tokenOptions.SecurityKey),
RoleClaimType = ClaimTypes.Role,
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
opts.SaveToken = true;
opts.Events = new JwtBearerEvents();
opts.Events.OnMessageReceived = context =>
{
if (context.Request.Cookies.ContainsKey("X-Access-Token"))
{
context.Token = context.Request.Cookies["X-Access-Token"];
}
return Task.CompletedTask;
};
})
.AddCookie(options =>
{
options.Cookie.HttpOnly=true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.IsEssential = true;
});
Reflesh token
private async Task<JsonConvertModelNoList<TokenDto>> TokenIsValid(HttpResponseMessage response)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
_httpContextAccessor.HttpContext.Response.Cookies.Delete("X-Access-Token");
_httpContextAccessor.HttpContext.Response.Cookies.Delete("X-Refresh-Token");
var refToken = new RefreshTokenDto();
refToken.Token = _httpContextAccessor.HttpContext.Request.Cookies["X-Refresh-Token"];
var refTokenResponse = await _httpClient.PostAsync("https://localhost:44320/api/Auth/CreateTokenByRefreshToken", refToken,
new JsonMediaTypeFormatter()).ConfigureAwait(false);
JsonConvertModelNoList<TokenDto> result = null;
result = JsonConvert.DeserializeObject<JsonConvertModelNoList<TokenDto>>(await refTokenResponse.Content.ReadAsStringAsync());
if (refTokenResponse.IsSuccessStatusCode)
{
if (result.StatusCode == 200)
{
_httpContextAccessor.HttpContext.Response.Cookies.Append("X-Access-Token", result.Data.AccessToken, new CookieOptions() { HttpOnly = true, SameSite = SameSiteMode.Strict });
_httpContextAccessor.HttpContext.Response.Cookies.Append("X-Refresh-Token", result.Data.RefreshToken, new CookieOptions() { HttpOnly = true, SameSite = SameSiteMode.Strict });
return result;
}
else
{
var errorResult = JsonConvert.DeserializeObject<JsonConvertModelError>(result.Error.ToString());
result.Error = errorResult.errors;
return result;
}
}
else
{
var errorResult = JsonConvert.DeserializeObject<JsonConvertModelError>(result.Error.ToString());
result.Error = errorResult.errors;
return result;
}
}
return null;
}
Does anyone have any idea how to go about it? My English is a little bad. Sorry.
Related
I try to login User from Microsoft account into my app.
First : I'm following this info, and that'a working well.
https://learn.microsoft.com/fr-fr/azure/active-directory/develop/web-app-quickstart?pivots=devlang-aspnet-core
But, when I try merge it with the classic Identity Login, on the external Login, this function return null all the time. And the login info on the starup is correct. I think it was a schema problem but I try to change it and it was not.
var info = await _signInManager.GetExternalLoginInfoAsync();
public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
var userl = _signInManager.IsSignedIn(User);
//ExternalLoginInfo info = new ExternalLoginInfo(User, "Microsoft", User.Claims.Where(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").FirstOrDefault().Value.ToString(), "Microsoft");
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToPage("./Lockout");
}
else
{
// If the user does not have an account, then ask the user to create an account.
ReturnUrl = returnUrl;
ProviderDisplayName = info.ProviderDisplayName;
if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
{
Input = new InputModel
{
Email = info.Principal.FindFirstValue(ClaimTypes.Email)
};
}
return Page();
}
}
Startup.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDbContext<DatabaseContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddErrorDescriber<FrenchIdentityErrorDescriber>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services
.AddControllersWithViews()
.AddRazorRuntimeCompilation()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
builder.Services.AddMvc();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(8);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
//options.Cookie.Name = "WebSignature.Session";
});
builder.Services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.User.RequireUniqueEmail = false;
});
builder.Services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromHours(12);
options.LoginPath = ApplicationRedirection.PageConnexion;
options.LogoutPath = "/Identity/Account/Logout";
options.AccessDeniedPath = "/Erreurs/403";
options.SlidingExpiration = true;
});
builder.Services
.AddAuthentication()
.AddJwtBearer(options =>
{
var SecretKey = builder.Configuration.GetSection("JwtToken").GetValue<string>("SecretKey") ?? string.Empty;
var Audience = builder.Configuration.GetSection("JwtToken").GetValue<string>("Audience") ?? string.Empty;
var Issuer = builder.Configuration.GetSection("JwtToken").GetValue<string>("Issuer") ?? string.Empty;
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.Zero,
ValidateAudience = true,
ValidAudience = Audience,
ValidateIssuer = true,
ValidIssuer = Issuer,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey))
};
});
builder.Services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options =>
{
var config = builder.Configuration.GetSection("AzureAd");
options.Instance = config["Instance"];
options.Domain = config["Domain"];
options.ClientId = config["ClientId"];
options.TenantId = config["TenantId"];
options.ClientSecret = config["ClientSecret"];
options.CallbackPath = config["CallbackPath"];
options.RequireHttpsMetadata = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UsePkce = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true
};
})
.EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' '))
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(PolicyApplication.Administrateur, policy => policy.RequireAuthenticatedUser().RequireRole(RoleUtilisateur.Administrateur)/*.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme)*/);
options.AddPolicy(PolicyApplication.Utilisateur, policy => policy.RequireAuthenticatedUser().RequireRole(RoleUtilisateur.Utilisateur)/*.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme)*/);
/// Récupère tous les roles de la class
var AllRole = typeof(RoleUtilisateur).GetFields().Select(x => x.GetValue(x.Name)?.ToString() ?? "").ToArray();
options.AddPolicy(PolicyApplication.All, policy => policy.RequireAuthenticatedUser().RequireRole(AllRole)/*.AddAuthenticationSchemes(IdentityConstants.ApplicationScheme)*/);
/// Pour l'API
options.AddPolicy(PolicyApplication.API, policy => policy.RequireAuthenticatedUser().AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme));
});
builder.Services.Configure<GzipCompressionProviderOptions>(o => o.Level = CompressionLevel.Optimal);
builder.Services.AddKendo();
builder.Services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
var app = builder.Build();
var supportedCultures = new[] { new CultureInfo("fr-FR") };
app.UseRequestLocalization(new RequestLocalizationOptions()
{
DefaultRequestCulture = new RequestCulture(new CultureInfo("fr-FR")),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseSwagger();
app.UseSwaggerUI();
app.UseStatusCodePages(context =>
{
...
return Task.CompletedTask;
});
app.UseSession();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.Run();
Value Null
I the problem is due to the claims who was empty. But i don't know why.
enter image description here
So what is wrong, any ideas ?
Finally I found.
Simply use that
builder.Services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
var config = builder.Configuration.GetSection("AzureAd");
options.ClientId = config["ClientId"] ?? string.Empty;
options.ClientSecret = config["ClientSecret"] ?? string.Empty;
options.CallbackPath = config["CallbackPath"] ?? string.Empty;
});
than
builder.Services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options =>
{
var config = builder.Configuration.GetSection("AzureAd");
options.Instance = config["Instance"];
options.Domain = config["Domain"];
options.ClientId = config["ClientId"];
options.TenantId = config["TenantId"];
options.ClientSecret = config["ClientSecret"];
options.CallbackPath = config["CallbackPath"];
options.RequireHttpsMetadata = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UsePkce = true;
options.ResponseType = OpenIdConnectResponseType.Code;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true
};
})
.EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' '))
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
I have first started with a cookie authentication and somehow after also enabling JWT it doesnt let me to retrieve the user by the User.Identity, object
and I found this article which suggested that you can enable both
https://weblog.west-wind.com/posts/2022/Mar/29/Combining-Bearer-Token-and-Cookie-Auth-in-ASPNET
this is my program cs on the Authentication config
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = "JWT_OR_COOKIE"/*"Identity.Application"*/;
options.DefaultChallengeScheme = "JWT_OR_COOKIE";
})
.AddCookie("Cookies", options =>
{
options.LoginPath = "/identity/account/login";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
})
.AddJwtBearer("Bearer",options =>
{
options.RequireHttpsMetadata = false;
options.Authority = "/Security/Token/Validate"; // TODO: Update URL
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience=builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"])),
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/ConnectionsHub")))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
})
.AddPolicyScheme("JWT_OR_COOKIE", "JWT_OR_COOKIE", options =>
{
// runs on each request
options.ForwardDefaultSelector = context =>
{
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
return "Bearer";
return "Cookies";
};
});
...
app.UseAuthentication();
app.UseAuthorization();
I have 3 projects JWT.IDP, JWT.API, JWT.MVC.
JWT.IDP - an API project validates user and issues the JWT token.
JWT.API - an API project for my business logic, CURD etc
JWT.MVC - an MVC application for UI.
My intention is to use this token generated in JWT.IDP and call the JWT.API functions from JWT.MVC
The IDP token is working perfectly fine, I can generate the token and my JWT.MVC Login controller is able to receive it. But when I am trying to use this token to access the JWT.API it gives a 500 error (Please see the last function in the below code (GetWeatherData)).
Can someone help, I am not an advanced user, the code written below is taken from several samples. So I am not sure whether it really is the right code.
namespace JWT.MVC.Controllers
{
public class LoginController : Controller
{
public IActionResult DoLogin()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DoLogin([Bind("EmailOrName,Password")] LoginRequestModel loginRequestModel)
{
var apiName = $"https://localhost:44318/api/User/login";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, loginRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<IEnumerable<AccessibleDb>>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
foreach (var item in data)
{
item.UserName = loginRequestModel.EmailOrName;
}
return View("SelectDatabase" , data);
}
public async Task<IActionResult> PostLogin(string db, string user)
{
TokenRequestModel tokenRequestModel = new TokenRequestModel() { Database = db, UserName = user };
var apiName = $"https://localhost:44318/api/User/tokenonly";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, tokenRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<AuthenticationModel>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var stream = data.Token;
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(stream);
var tokenS = jsonToken as JwtSecurityToken;
var selectedDb = tokenS.Claims.First(claim => claim.Type == "Database").Value;
ViewBag.SelectedDb = selectedDb;
return View(data);
}
public async Task<IActionResult> GetWeatherData(string token)
{
var apiName = $"https://localhost:44338/weatherforecast";
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await httpClient.GetAsync(apiName);
if (!response.IsSuccessStatusCode)
{
ViewBag.Error = response.StatusCode;
return View("Weatherdata");
}
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<WeatherForecast>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return View("Weatherdata" , data);
}
}
}
Startup class for JWT.MVC is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
Startup class for JWT.API is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Copy from IS4
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API", Version = "v1" });
});
}
Startup class for JWT.IDP is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Configuration from AppSettings
services.Configure<JwtSettings>(Configuration.GetSection("JWT"));
//User Manager Service
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>();
services.AddScoped<IUserService, UserService>();
//Adding DB Context with MSSQL
services.AddDbContext<IdentityDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDbConnectionString"),
b => b.MigrationsAssembly(typeof(IdentityDbContext).Assembly.FullName)));
//Adding Athentication - JWT
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(Convert.ToInt32(Configuration["JWT:DurationInMinutes"])),
ValidIssuer = Configuration["JWT:Issuer"],
ValidAudience = Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"]))
};
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.IDP", Version = "v1" });
});
}
And the JWT Setting is as below
"JWT": {
"key": "C1CF4B7DC4C4175B6618DE4F55CA4",
"Issuer": "http://localhost:44318",
"Audience": "SecureApiUser",
"DurationInMinutes": 60
},
It's quite surprising that no one was able to identify the mistake. I made the following changes and it works perfectly fine now.
The ConfigureServices is like below in both MVC and API projects. No other changes to any other codes.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var authenticationProviderKey = "IdentityApiKey";
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("C1CF4B7DC4C4175B6618DE4F55CA4"));
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = "http://localhost:44318",
ValidateAudience = true,
ValidAudience = "SecureApiUser",
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireExpirationTime = true,
};
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = authenticationProviderKey;
})
.AddJwtBearer(authenticationProviderKey, x =>
{
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = tokenValidationParameters;
});
//services.AddAuthentication(options =>
//{
// options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
// options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
//}).AddJwtBearer(options =>
//{
// options.Authority = "https://localhost:44318"; ;
// options.RequireHttpsMetadata = false;
// options.Audience = "SecureApiUser";
//});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API2", Version = "v1" });
});
}
I am trying to create a custom authentication handler that will require the Bearer JWT in the body of an HTTP request, but I'd prefer not to create a whole new custom authorization. Unfortunately, the only thing I can do is read the HTTP request body, get the token from there and put it in the Authorization header of the request.
Is there a different, more efficient way to do it? All I managed is to find the default JwtBearerHandler implementation on GitHub but when I make some modifications, it can't read the principal properly.
Startup.cs:
services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = ctx =>
{
if (ctx.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
ctx.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
public class AuthHandler : JwtBearerHandler
{
private readonly IRepositoryEvonaUser _repositoryUser;
private OpenIdConnectConfiguration _configuration;
public AuthHandler(IOptionsMonitor<JwtBearerOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IDataProtectionProvider dataProtection,
ISystemClock clock,
IRepositoryUser repositoryUser,
OpenIdConnectConfiguration configuration
)
: base(options, logger, encoder, dataProtection, clock)
{
_repositoryUser = repositoryUser;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
try
{
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
return messageReceivedContext.Result;
}
token = messageReceivedContext.Token;
if (string.IsNullOrEmpty(token))
{
Request.EnableBuffering();
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, true, 10, true))
{
var jsonBody = reader.ReadToEnd();
var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
if (body != null)
{
token = body.Token;
}
Request.Body.Position = 0;
}
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
var validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
var issuers = new[] { _configuration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
}
List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(token))
{
ClaimsPrincipal principal; // it can't find this
try
{
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
if (validationFailures == null)
{
validationFailures = new List<Exception>(1);
}
validationFailures.Add(ex);
continue;
}
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = validatedToken
};
await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
}
if (validationFailures != null)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}
return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
}
catch (Exception ex)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
throw;
}
}
}
Or, is there a way to just tell the application to expect a JWT in the HTTP request body? I am well aware that the token should be sent in the request header instead of body, but I am interested into seeing if (and if so, how) this can be implemented.
I also tried this:
OnMessageReceived = ctx =>
{
ctx.Request.EnableBuffering();
using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 10, true))
{
var jsonBody = reader.ReadToEnd();
var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
if (body != null)
{
ctx.Token = body.Token;
ctx.Request.Body.Position = 0;
}
}
return Task.CompletedTask;
}
By default , AddJwtBearer will get token from request header , you should write your logic to read token from request body and validate the token . That means no such configuration to "tell" middleware to read token form request body .
If token is sent in request body , you need to read the request body in middleware and put token in header before the jwt middleware reaches. Or read the request body in one of the jwt bearer middleware's event , for example , OnMessageReceived event , read token in request body and at last set token like : context.Token = token; . Here is code sample for reading request body in middleware .
I'll mark #Nan Yu's answer as the correct one, but I'll post my final code nonetheless. What I essentially did was revert back to the default JwtBearerHandler and use JwtBearerOptions and JwtBearerEvents's OnMessageReceived event to get the token value from HTTP request's body.
They all reside in the Microsoft.AspNetCore.Authentication.JwtBearer namespace.
services
.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
RequireExpirationTime = true,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = ctx =>
{
if (ctx.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
ctx.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
OnMessageReceived = ctx =>
{
ctx.Request.EnableBuffering();
using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 1024, true))
{
var jsonBody = reader.ReadToEnd();
var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
ctx.Request.Body.Position = 0;
if (body != null)
{
ctx.Token = body.Token;
}
}
return Task.CompletedTask;
}
};
});
My setup: I've created and have running a WebAPI solution that performs the authentication of a username and password against a source (currently a db). This generates the JWT token and returns it to the requesting app (a ASP.NET Core 2.2 app).
Most solutions talk of securing the WebAPI exposed methods but my approach is to only do the authentication through WebAPI. The individual apps need to accept the token so they can determine authorization.
Now the question: what is the best approach to reading the token from the WebAPI (which I've done already), validating it, and then storing it for any/all controllers to know there is an authenticated user (via Authorize attribute) so long as the token is valid?
Debugging this more, it seems my token is not being added to the headers. I see this debug message:
Authorization failed for the request at filter 'Microsoft.AspNet.Mvc.Filters.AuthorizeFilter'
Code Update2 - code that gets the JWT:
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri(_configuration.GetSection("SecurityApi:Url").Value);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//login
Task<HttpResponseMessage> response = ValidateUserAsync(client, username, password);
Task<Core.Identity.TokenViewModel> tokenResult = response.Result.Content.ReadAsAsync<Core.Identity.TokenViewModel>();
if (!response.Result.IsSuccessStatusCode)
{
if (tokenResult != null && tokenResult.Result != null)
{
ModelState.AddModelError("", tokenResult.Result.ReasonPhrase);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
}
return View();
}
JwtSecurityToken token = new JwtSecurityToken(tokenResult.Result.Token);
int userId;
if (int.TryParse(token.Claims.First(s => s.Type == JwtRegisteredClaimNames.NameId).Value, out userId))
{
//load app claims
Core.Identity.UserInfo userInfo = Core.Identity.UserLogin.GetUser(_identityCtx, userId);
Core.Identity.UserStore uStore = new Core.Identity.UserStore(_identityCtx);
IList<Claim> claims = uStore.GetClaimsAsync(userInfo, new System.Threading.CancellationToken(false)).Result;
claims.Add(new Claim(Core.Identity.PowerFleetClaims.PowerFleetBaseClaim, Core.Identity.PowerFleetClaims.BaseUri));
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);
//complete
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, tokenResult.Result.Token);
//var stuff = HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, principal, authProperties);
}
else
{
ModelState.AddModelError("", AppStrings.InvalidLoginError);
return View();
}
return RedirectToAction("Index", "Home");
Startup:
private void ConfigureIdentityServices(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
Core.Startup authStart = new Core.Startup(this.Configuration);
authStart.ConfigureAuthorizationServices(services);
}
Auth:
public void ConfigureAuthorizationServices(IServiceCollection services)
{
services.AddDbContext<Identity.IdentityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SecurityConn")));
services.AddScoped<DbContext, Identity.IdentityContext>(f =>
{
return f.GetService<Identity.IdentityContext>();
});
services.AddIdentityCore<Identity.UserInfo>().AddEntityFrameworkStores<Identity.IdentityContext>().AddRoles<Identity.Role>();
services.AddTransient<IUserClaimStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IUserRoleStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
services.AddTransient<IRoleStore<Core.Identity.Role>, Core.Identity.RoleStore>();
services.AddAuthorization(auth =>
{
auth.AddPolicy(JwtBearerDefaults.AuthenticationScheme, new AuthorizationPolicyBuilder().AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build());
auth.AddPolicy(PFBaseClaim, policy => policy.RequireClaim(Identity.PFClaims.BaseUri));
});
}
In the end, my approach was to use a secure cookie and a base claim to prove the user authenticated.
private void ConfigureAuthentication(IServiceCollection services)
{
services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");
//authentication token
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/Login";
opt.AccessDeniedPath = "/Login";
opt.LogoutPath = "/Login/Logoff";
opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuer = true,
ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
ValidateLifetime = true
};
});
}
And at login:
AuthenticationProperties authProperties = new AuthenticationProperties();
authProperties.ExpiresUtc = token.ValidTo;
authProperties.AllowRefresh = false;
authProperties.IsPersistent = true;
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, userStore.CreateAsync(user).Result, authProperties);
return RedirectToAction("Index", "Home");