I have been trying to getting this fixed for many times already, but have no success so far. I am logging into my page and everything works fine. However every time I am reloading my page I am ending up signed out and redirected to "Login page" and have to sign in again. What is the problem? Is there is something messed up with my Coockie logic?
I have also tried to implement login.RememberMe logic, but it does not work either. I have checked that login.RememberMe returns true, but no effect.
Login method in controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginModel login)
{
ApplicationUser user = await this.SignInManager.UserManager.FindByEmailAsync(login.Email);
Microsoft.AspNetCore.Identity.SignInResult result =
await this.SignInManager.PasswordSignInAsync(login.Email, login.Password, login.RememberMe, false);
if (!result.Succeeded)
{
List<string> errors = new List<string>();
errors.Add("Email and password are invalid.");
return BadRequest(new LoginResult
{
Successful = false,
Errors = errors,
});
}
IList<string> roles = await this.SignInManager.UserManager.GetRolesAsync(user);
List<Claim> claims = new List<Claim>
{
new Claim(ClaimTypes.Name, login.Email)
};
ClaimsIdentity identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(identity);
AuthenticationProperties props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(1)
};
// to register the cookie to the browser
this.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();
foreach (string role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["JwtSecurityKey"]));
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
DateTime expiry = DateTime.Now.AddDays(Convert.ToInt32(this.Configuration["JwtExpiryInDays"]));
JwtSecurityToken token = new JwtSecurityToken(
this.Configuration["JwtIssuer"],
this.Configuration["JwtAudience"],
claims,
expires: expiry,
signingCredentials: creds
);
return Ok(new LoginResult
{
Successful = true,
Token = new JwtSecurityTokenHandler().WriteToken(token),
});
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
})
.AddCookie(options =>
{
options.Cookie.Name = "DashCoockie";
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
options.EventsType = typeof(CookieAuthEvent);
});
services.AddScoped<CookieAuthEvent>();
services.AddAuthorization(config =>
{
config.AddPolicy(Policies.IsAdmin, Policies.IsAdminPolicy());
config.AddPolicy(Policies.IsUser, Policies.IsUserPolicy());
});
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
services.AddControllers();
// Instant update on runtime for development purposes
services.AddControllersWithViews().AddRazorRuntimeCompilation();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
app.UseCookiePolicy();
}
CookieAuthEvent.cs:
public class CookieAuthEvent : CookieAuthenticationEvents
{
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
context.Request.HttpContext.Items.Add("ExpiresUTC", context.Properties.ExpiresUtc);
}
}
After logging in (Coockie name is the same, just hided first part as it is in the code above):
My project structure is as follows: Client, Server, Shared. I found solution by using LocalStorage and AuthService : IAuthService on Client side. So is it so that in my setup the only solution is to use LocalStorage?
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly ILocalStorageService _localStorage;
public AuthService(HttpClient httpClient,
AuthenticationStateProvider authenticationStateProvider,
ILocalStorageService localStorage)
{
_httpClient = httpClient;
_authenticationStateProvider = authenticationStateProvider;
_localStorage = localStorage;
}
public async Task<RegisterResult> Register(RegisterModel registerModel)
{
var result = await _httpClient.PostJsonAsync<RegisterResult>("api/accounts", registerModel);
return result;
}
public async Task<LoginResult> Login(LoginModel loginModel)
{
var loginAsJson = JsonSerializer.Serialize(loginModel);
var response = await _httpClient.PostAsync("api/Login", new StringContent(loginAsJson, Encoding.UTF8, "application/json"));
var loginResult = JsonSerializer.Deserialize<LoginResult>(await response.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (!response.IsSuccessStatusCode)
{
return loginResult;
}
await _localStorage.SetItemAsync("authToken", loginResult.Token);
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(loginModel.Email);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", loginResult.Token);
return loginResult;
}
public async Task Logout()
{
await _localStorage.RemoveItemAsync("authToken");
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();
_httpClient.DefaultRequestHeaders.Authorization = null;
}
}
It seems that you didn't set the token in your request header
try add these codes in your startup class:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["access_token"];
return Task.CompletedTask;
}
};
});
In your Login Action :
Response.Cookies.Append("access_token",your JwtSecurityToken)
Related
I am trying to setup a custom authorization handler in my project closely following the Resource-based authorization in ASP.NET Core Microsoft documentation. However, calling the API endpoint always returns a 403 response and doesn't even trigger a breakpoint in my handler.
Feels like I quadruple checked every single thing yet it still doesn't work. Am I blind or is there something wrong with my code?
Here's my custom handler:
public class TripAuthorizationHandler : AuthorizationHandler<SameUserRequirement, Trip>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SameUserRequirement requirement, Trip resource)
{
if(context.User.FindFirst("userId").Value == resource.ApplicationUserId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class SameUserRequirement : IAuthorizationRequirement{}
Program.cs file:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]))
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SameUser", policy => policy.Requirements.Add(new SameUserRequirement()));
});
builder.Services.AddTransient<IAuthorizationHandler, TripAuthorizationHandler>();
builder.Services.AddTransient<IAuthenticationService, AuthenticationService>();
builder.Services.AddTransient<ITokenService, TokenService>();
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
And here's the controller:
[Authorize(Policy = "SameUser")]
[HttpPut("trips/{tripId}")]
public async Task<IActionResult> UpdateTrip([FromBody] TripPostDto request, int tripId)
{
var trip = await _tripService.GetByIdAsync(tripId);
var authorizationResult = await _authorizationService.AuthorizeAsync(User, trip, "SameUser");
if (authorizationResult.Succeeded)
{
await _tripService.UpdateAsync(tripId, request);
return NoContent();
}
else if (User.Identity.IsAuthenticated)
{
return Forbid();
}
else
{
return Challenge();
}
}
Edit:
Here's my implementation of AuthenticationService
public class AuthenticationService : IAuthenticationService
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ITokenService _tokenService;
private readonly IMapper _mapper;
public AuthenticationService(UserManager<ApplicationUser> userManager, IMapper mapper, ITokenService tokenService)
{
_userManager = userManager;
_mapper = mapper;
_tokenService = tokenService;
}
public async Task<SuccessfulLoginDto> LoginAsync(LoginUserDto loginUserDto)
{
var user = await _userManager.FindByEmailAsync(loginUserDto.Email);
if(user == null)
{
throw new ValidationException("Invalid login data!");
}
var passwordValid = await _userManager.CheckPasswordAsync(user, loginUserDto.Password);
if (!passwordValid)
{
throw new ValidationException("Invalid login data!");
}
var accessToken = await _tokenService.GetTokenAsync(user);
return new SuccessfulLoginDto() { Token = accessToken.AccessToken };
}
public async Task RegisterAsync(RegisterUserDto registerUserDto)
{
var existingUser = await _userManager.FindByEmailAsync(registerUserDto.Email);
if (existingUser != null)
{
throw new UserAlreadyExistsException("User with this email already exists!");
}
var newUser = _mapper.Map<ApplicationUser>(registerUserDto);
var result = await _userManager.CreateAsync(newUser, registerUserDto.Password);
if (!result.Succeeded)
{
throw new ValidationException(string.Join(" ", result.Errors.Select(e => e.Description)));
}
await _userManager.AddToRoleAsync(newUser, Authorization.Roles.User.ToString());
}
}
Have you tried ordering the service injection before?
Order matters in Startup
builder.Services.AddTransient<IAuthorizationHandler, TripAuthorizationHandler>();
builder.Services.AddTransient<IAuthenticationService, AuthenticationService>();
builder.Services.AddTransient<ITokenService, TokenService>();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]))
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("SameUser", policy => policy.Requirements.Add(new SameUserRequirement()));
});
I am trying to use a bearer token at present and no matter what I do, I get a HTTP 401 unauthorized error.
I am following a guide on JWT implementation I have a extension method that handles the JWT.
public static class AuthenticationExtension
{
public static IServiceCollection AddTokenAuthentication(this IServiceCollection services, IConfiguration config)
{
var secret = config.GetSection("JwtConfig").GetSection("secret").Value;
var key = Encoding.ASCII.GetBytes(secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// ValidIssuer = "localhost",
//ValidAudience = "localhost"
};
});
return services;
}
}
Also the following is the way the token controller generates the token obv I should be getting values from the header instead email and a password for example.
As that code be replaced if found in code could it not.
var token = jwt.GenerateSecurityToken("fake#email.com");
In my StartUp.cs I simply have the following to add the middle ware to my config. In the services section I conduct a test
services.AddTokenAuthentication(Configuration);
But as you see I get HTTP 401 unauthorised returned.
This is the code from the api/token controller.
[Route("api/[controller]")]
[ApiController]
public class TokenController : ControllerBase
{
private IConfiguration _config;
public TokenController(IConfiguration config)
{
_config = config;
}
[HttpGet]
public string GetRandomToken()
{
var jwt = new JwtService(_config);
var token = jwt.GenerateSecurityToken("fake#email.com");
return token;
}
}
Token from api/token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZha2VAZW1haWwuY29tIiwibmJmIjoxNTg2NjMzMTkwLCJleHAiOjE1ODY3MTk1OTAsImlhdCI6MTU4NjYzMzE5MH0.SPSErcPpD4f48sWFYQFVUBmTaVtCW8oDw4Np6Tncozo
This is my appSettings.json config I will of course be changing my secret once I have this setup.
Is this enough to secure a api or should you also use client id and secret in terms of api layer. HMAC style.
"JwtConfig": {
"secret": "PDv7DrqznYL6nv7DrqzjnQYO9JxIsWdcjnQYL6nu0f",
"expirationInMinutes": 1440
},
In my case I also failed like you did at first, eventually I followed this tutorial and with a few modifications I had a working code.
I used user Id as identifier and here are the parts in my code related to jwt authentication:
UserService:
public class UserService : IUserService
{
...
public IDataResult<AuthenticationResponse> Authenticate(AuthenticationRequest request)
{
UserDto userDto = _mapper.Map<UserDto>(user);
AccessToken token = generateJwtToken(userDto);
return new SuccessDataResult<AuthenticationResponse>(new AuthenticationResponse(userDto, token));
}
private AccessToken generateJwtToken(UserDto userDto)
{
AuthUser authUser = _mapper.Map<AuthUser>(userDto);
return _jwtHelper.CreateToken(authUser);
}
...
}
JWTHelper:
public class JwtHelper
{
...
public AccessToken CreateToken(AuthUser user)
{
_accessTokenExpiration = DateTime.Now.AddMinutes(_tokenOptions.ExpirationInMinutes);
var securityKey = SecurityKeyHelper.CreateSecurityKey(_tokenOptions.SecurityKey);
var signingCredentials = SigningCredentialsHelper.CreateSigningCredentials(securityKey);
var jwt = CreateJwtSecurityToken(_tokenOptions, user, signingCredentials);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return new AccessToken
{
Token = token,
Expiration = _accessTokenExpiration
};
}
private JwtSecurityToken CreateJwtSecurityToken(TokenOptions tokenOptions, AuthUser user,
SigningCredentials signingCredentials)
{
var jwt = new JwtSecurityToken(
expires: _accessTokenExpiration,
claims: SetClaims(user),
signingCredentials: signingCredentials
);
return jwt;
}
private IEnumerable<Claim> SetClaims(AuthUser user)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString());
return claims;
}
}
Startup.cs
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetValue<string>("TokenOptions:SecurityKey")))
};
});
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(...);
...
}
}
It is my first time to use ASP.NET core 2.2 Identity to manage Authorization in Controller. In the client side, I use (Login.cshtml.cs) SignInManger.PasswordSignInAsync(…) to sign in a user. After user sign in successfully, I send HttpClient call to UsersController in another project to get more user’s info. However, the Controller is does not receive the the Claims.ClaimsPrincipal (User) information from the Request. The [Authorize(Roles = “admin”)] fails but [AllowAnonymous] passes. I checked the Request cookie. The User HttpContext is not updated to the signed in user in client side. To be exact, I don’t know how to update the User in cookie. I thought it is done in the background. I have looked through many posts but none of them explicitly mentions how the User is being updated in the cookie and send to the controller. It seems that the User updated is done after the program exits the Login.cshtml.cs. I am able to get the Identity of the user by using SingInManager.CreateUserPrincipalAsync(…) within public async Task OnPostAsync(string returnUrl = null) of Login.cshtml.cs. But I don’t know how to pass that Identity to the User in cookie and send it to Controller.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
// test: get the Identity of the Input.Email
var user = await _signInManager.UserManager.FindByEmailAsync(Input.Email);
var userPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
var identity = userPrincipal.Identity;
// * User Identity does not populate at this point.
var ret = await GetRestOfUserInfo();
}
}
return Page();
}
private async Task<int> GetRestOfUserInfo()
{
var userStatus = await _userManager.GetUserAsync(User);
UserCSDto userCDDto = new UserCSDto()
{
Email = Input.Email,
UserName = Input.Email,
Password = Input.Password,
RememberMe = Input.RememberMe
};
CookieContainer cookieContainer = new CookieContainer();
HttpClientHandler handler = new HttpClientHandler
{
UseCookies = true,
UseDefaultCredentials = true,
CookieContainer = cookieContainer
};
var client = new HttpClient(handler);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var content = new StringContent(JsonConvert.SerializeObject(userCDDto), Encoding.UTF8, "application/json");
var url = _connectionSettings.UrlConnection + "/api/users/authenticate";
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode)
{
var respUser = JsonConvert.DeserializeObject<User>(response.Content.ReadAsStringAsync().Result);
var _responseToken = response.Headers.Where(t => t.Key == "Authorization")
.Select(t => t.Value);
var token = (_responseToken.FirstOrDefault()).FirstOrDefault();
applicationUser.PreferredName = respUser.PreferredName;
applicationUser.Token = token;
ApplicationUser user = await _userManager.FindByEmailAsync(Input.Email);
user.Token = token;
user.PreferredName = respUser.PreferredName ?? "";
IEnumerable<Claim> claims = new Claim[]
{
new Claim("Token", token),
new Claim("PreferredName", "123abc") // respUser.PreferredName)
};
await _userManager.AddClaimsAsync(user, claims);
return 1;
}
return 0;
}
[Authorize]
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class UsersController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private IUserService _userService;
public UsersController(UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
:
:
[Authorize(Roles = "any role")] // failed because User does not contain any info
[AllowAnonymous] // works because no authorization check
[HttpPost("authenticate")]
public async Task<IActionResult> Authenticate([FromBody]UserCSDto userParam)
{
var username = User.Identity.Name; // username is null because User does not
// have info.
var user = _userService.Authenticate(userParam.UserName, userParam.Password);
if (user == null)
return BadRequest(new { message = "Username or passwrod is incorrect" });
var token = _jwtTokenService.CreateToken(userCSDto);
Response.Headers.Add("Authorization", "Bearer " + token);
Response.Headers.Add("Content-Type", "application/json");
return Ok(userCSDto);
}
:
:
}
/////////////////////////////
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConfiguration>(Configuration);
services.Configure<ConnectionSettings>(Configuration.GetSection("ConnectionStrings"));
var connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<EFCoreContext>(
options => options.UseSqlServer(connection,
b => b.MigrationsAssembly("EFStructures"))); // from Data project
services.AddHttpContextAccessor(); // #.Net Core 2.2
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Expiration = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
});
services.AddSession(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Name = "Test.Session";
options.IdleTimeout = TimeSpan.FromMinutes(60);
options.Cookie.Path = "/";
});
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 = true;
// Require Confirmed and Unique Email
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = false;
});
#region authenication using JWT token
services.Configure<JwtTokenSettings>(Configuration.GetSection("JwtTokenSettings"));
///// configure jwt authentication
var jwtTokenSettings = Configuration.GetSection("JwtTokenSettings").Get<JwtTokenSettings>();
var key = Encoding.ASCII.GetBytes(jwtTokenSettings.Secret);
//Adds cookie middleware to the services collection and configures it
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => options.LoginPath = new PathString("/account/login"));
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
//ValidateIssuer = Configuration["AppSettings:Issuer"],
//ValidateAudience = Configuration["AppSettings:Issuer"],
ValidateLifetime = true
};
});
#endregion
#region Add Role services to Identity RAZOR PAGE only
services.AddIdentity<ApplicationUser, IdentityRole>()
// .AddDefaultUI(Microsoft.AspNetCore.Identity.UI.UIFramework.Bootstrap4)
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<EFCoreContext>();
#endregion
#region Setting for using Identity
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
#endregion Setting for using Identity
services.AddHttpClient();
services.AddScoped<IJwtTokenService, JwtTokenService>();
services.AddScoped<IEmailSenderService, EmailSenderService>();
services.AddScoped<IEmailSender, EmailSender>();
#region Get EmailSettings from appsettings.json
services.AddOptions();
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
#endregion Get EmailSettings from appsettings.json
services.AddMvc()
.ConfigureApiBehaviorOptions(options =>
{
options
.SuppressUseValidationProblemDetailsForInvalidModelStateResponses = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseAuthentication();
app.UseSession();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRequestLocalization();
app.UseMvcWithDefaultRoute();
// global cors policy
app.UseCors(x => x
.WithOrigins("https://localhost")
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseMvc();
}
}
I am trying to implement Jwt Authentication for my application and so far I have managed to generate the token and when I decoded it, it looks fine. However, when I use Postman to try to access areas of my code that is for Authorized only, I get an UnAuthorized error. I have discovered that its because of a setting in the Startup.cs.
Startup.cs
var key = Encoding.ASCII.GetBytes(Configuration["JwtAuthentication:SecurityKey"]);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidIssuer = Configuration["JwtAuthentication:Issuer"],
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true
};
});
If I set ValidateIssuer to false, I can get access to the [Authorized] controller, otherwise it will block me from it. Also, ValidateLifeTime doesn't seem to work for me. It will continue to show the result even though the token has expired.
This is how I generate my token in another class
public class GenerateToken : Controller
{
private IConfiguration configuration;
public GenerateToken(IConfiguration configuration)
{
this.configuration = configuration;
}
public String Generate(GenerateTokenViewModel Input)
{
var tokenHandler = new JwtSecurityTokenHandler();
List<Claim> claims = new List<Claim>();
claims.Add(new Claim("UserName", Input.User.UserName));
claims.Add(new Claim("Email", Input.User.Email));
claims.Add(new Claim("PhoneNumber", Input.User.PhoneNumber));
claims.Add(new Claim("FirstName", Input.User.FirstName));
claims.Add(new Claim("LastName", Input.User.LastName));
claims.Add(new Claim("Id", Input.User.Id));
foreach (var item in Input.Roles)
{
var currentItem = new UserRoleDetailsViewModel
{
Id = item.Id,
Name = item.Name,
ApplicationId = item.ApplicationId,
ApplicationName = item.ApplicationName
};
var convertedItem = JsonConvert.SerializeObject(currentItem);
claims.Add(new Claim("Roles", convertedItem));
}
var key = Encoding.ASCII.GetBytes(configuration["JwtAuthentication:SecurityKey"]);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Issuer = configuration["JwtAuthentication:Issuer"],
Audience = configuration["JwtAuthentication:Audience"],
IssuedAt = DateTime.UtcNow,
NotBefore = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var output = tokenHandler.WriteToken(token);
return output;
}
}
I'm not sure what I can do to solve it. Alternatively, is there a guide to generating and validating jwt for .net core 2.1?
EDIT:
Added Controller
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class AccountsController : ControllerBase
{
private AccountsData accountsData;
private readonly ILogger<AccountsController> logger;
private UserRolesData userRolesData;
private GenerateToken generateToken;
public AccountsController(ILogger<AccountsController> logger, GenerateToken generateToken, AccountsData accountsData, UserRolesData userRolesData)
{
this.accountsData = accountsData;
this.logger = logger;
this.userRolesData = userRolesData;
this.generateToken = generateToken;
}
[HttpGet]
[AllowAnonymous]
[Route("Item")]
public ActionResult Item()
{
return Ok("test");
}
[HttpGet]
[Route("GetThis")]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
[HttpPost]
[AllowAnonymous]
[Route("Authenticate")]
public ActionResult Authenticate([FromBody]AccountsDto Input)
{
LoginViewModel lvm = new LoginViewModel
{
Email = Input.Email,
Password = Input.Password
};
var user = accountsData.Authenticate(lvm);
var token = "";
if(user == null)
{
return NotFound(LoggingGlobals.NotFound);
}
else
{
var roles = userRolesData.GetUserRoles(user.Id);
GenerateTokenViewModel gtvm = new GenerateTokenViewModel
{
User = user,
Roles = roles
};
token = generateToken.Generate(gtvm);
}
return Ok(token);
}
}
I am not entirely sure what is causing the problem you are facing. I did notice that you have ValidateAudience = true but do not provide a ValidAudience.
In any case, here is a demo that does work. You can find it on GitHub here. These are the most relevant snippets of code.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var key = Encoding.ASCII.GetBytes("this-is-the-secret");
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuerSigningKey = true,
//
ValidIssuer = "my-auth-server",
ValidateIssuer = true,
//
ValidAudience = "my-resource-server",
ValidateAudience = true,
//
ValidateLifetime = true,
};
});
}
ValuesController.cs
public class ValuesController : ControllerBase
{
[HttpGet("/api/values")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public ActionResult<string> Values()
{
return $"You have authorized access";
}
[HttpGet("/api/jwt")]
public ActionResult<string> Jwt()
{
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("this-is-the-secret"));
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] { /* add claims */}),
Issuer = "my-auth-server",
Audience = "my-resource-server",
SigningCredentials =
new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature),
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddDays(1)
};
var jwtHandler = new JwtSecurityTokenHandler();
var token = jwtHandler.CreateToken(descriptor);
return jwtHandler.WriteToken(token);
// alternatively
// return jwtHandler.CreateEncodedJwt(descriptor);
}
}
I'm using the [Authorize] attribute for authentification in my controller, but when I get a request to TestMethod I get an error: "500 Internal..".
What am I doing wrong??
That my code from StartUp.cs
services.AddAuthorization(options =>
{
options.DefaultPolicy =
new AuthorizationPolicyBuilder("Identity.Application")
.RequireAuthenticatedUser()
.Build();
});
services
.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters =
new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
SaveSigninToken = true,
ValidateIssuer = true,
ValidIssuer = "http://blabla/",
ValidateAudience = true,
ValidAudience = "http://blabla/",
ValidateLifetime = true,
IssuerSigningKey = blabla.bla(),
ValidateIssuerSigningKey = true,
};
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
services.AddMvc();
And also code from Controller
[Route("test"), HttpPost]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public void Test() { }
Do you have ideas?
I'm using these libraries to generate the token:
System.IdentityModel.Tokens.Jwt;
Microsoft.IdentityModel.Tokens;
If you want to use the [Authorize] attribute you need to make a policy:
//new policy makes [Authorize] availible by claims
services.AddAuthorization((options) => {
options.AddPolicy("MyNewPolicy", policybuilder =>
{
policybuilder.RequireAuthenticatedUser();
policybuilder.RequireClaim("role", "someClaim");
});
});
//usage
[Authorize(Roles = "someClaim")]
public async Task<IActionResult> About(){
}
//to awnsr your comment add a list of claims to your user class ex:
new TestUser
{
SubjectId="1001",
Username="Frank",
Password="password",
Claims= new List<Claim>
{
new Claim("given_name","Frank"),
new Claim("family_name","Underwood"),
new Claim("address","1 addy rd unit 233"),
new Claim("role", "someClaim")
}
}
I ran into a lot of issues when tryout out AddJwtBearer. Finally I found out that making a manual login wasn't that much harder, worked easily and is also easier to debug.
Basically, first I created a helper class for creating and validating tokens. Here is the source code for the class: https://github.com/neville-nazerane/netcore-jwt-sample/blob/master/website/TokenGenerator.cs. Everything you had added in your TokenValidationParameters can go inside this class.
Once you have that, here is a Boiler plate authentication scheme:
public class TokenAuthenticationOptions : AuthenticationSchemeOptions
{
}
public class TokenAuthentication : AuthenticationHandler<TokenAuthenticationOptions>
{
public const string SchemeName = "TokenAuth";
public TokenAuthentication(IOptionsMonitor<TokenAuthenticationOptions> options, ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.Run(() => Authenticate());
}
private AuthenticateResult Authenticate()
{
string auth, token;
auth = Context.Request.Headers["Authorization"];
if (auth == null) return AuthenticateResult.Fail("No JWT token provided");
var auths = auth.Split(" ");
if (auths[0].ToLower() != "bearer") return AuthenticateResult.Fail("Invalid authentication");
token = auths[1];
try
{
var generator = new TokenGenerator();
var principal = generator.Validate(token);
return AuthenticateResult.Success(new AuthenticationTicket(principal, SchemeName));
}
catch
{
return AuthenticateResult.Fail("Failed to validate token");
}
}
}
Finally, in your start up you can use this scheme this way:
services.AddAuthentication(TokenAuthentication.SchemeName)
.AddScheme<TokenAuthenticationOptions, TokenAuthentication>
(TokenAuthentication.SchemeName, o => { });