Using CookieAuthentication plus custom scheme in Asp.net core - asp.net-core

So far my project uses Cookie authentication with a call like
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(
options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
if (string.IsNullOrEmpty(context.HttpContext?.User?.Identity?.Name))
context.Response.StatusCode = 401;
else
context.Response.StatusCode = 403;
return Task.CompletedTask;
};
})
;
I would like to extend this so I can use BasicAuth as an alternative to Cookies.
So I changed the above to
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication",
options => { })
.AddCookie(
options =>
{
options.Events.OnRedirectToLogin = context =>
{
//when defaultScheme is CookieAuthenticationDefaults.AuthenticationScheme, I come here when using BasicAuth
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
if (string.IsNullOrEmpty(context.HttpContext?.User?.Identity?.Name))
context.Response.StatusCode = 401;
else
context.Response.StatusCode = 403;
return Task.CompletedTask;
};
})
;
with
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
Response.Headers.Add("WWW-Authenticate", "Basic");
// Get authorization key
var authorizationHeader = Request.Headers["Authorization"].ToString();
var authHeaderRegex = new Regex(#"Basic (.*)");
if (!authHeaderRegex.IsMatch(authorizationHeader))
{
return Task.FromResult(AuthenticateResult.Fail("Authorization code not formatted properly."));
}
var authBase64 = Encoding.UTF8.GetString(Convert.FromBase64String(authHeaderRegex.Replace(authorizationHeader, "$1")));
var authSplit = authBase64.Split(Convert.ToChar(":"), 2);
var authUsername = authSplit[0];
var authPassword = authSplit.Length > 1 ? authSplit[1] : throw new Exception("Unable to get password");
if (authUsername != "myuser" || authPassword != "mypass")
{
return Task.FromResult(AuthenticateResult.Fail("The username or password is not correct."));
}
var claims = new List<Claim> { new Claim(ClaimTypes.Name, "crocoll") };
foreach (var role in new List<string>{"ProjectManager"})
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)));
}
}
but unfortunately my Handler's HandleAuthenticateAsync method does not get called as long as I have CookieAuthenticationDefaults.AuthenticationScheme as default scheme.
But changing default scheme to "BasicAuthentication" makes CookieAuthentication stop working.
What can I do to make both work in parallel?

Related

Trying to Authenticate using CookieAuthenticationDefaults.AuthenticationScheme with OpenIddict through angular client application

Trying to Authenticate using CookieAuthenticationDefaults.AuthenticationScheme with OpenIddict through angular client application but cookie not authenticated. does anyone know whats the issue
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginRequestDTO model)
{
var user = await _context.Users.Where(x => x.Username == model.Username).FirstOrDefaultAsync();
if(user == null)
{
return Ok(GenericResponse<bool>.Failure("Invalid Username or Password!", Models.Enums.ApiStatusCode.RecordNotFound));
}
else
{
if(user.Password != model.Password)
{
return Ok(GenericResponse<bool>.Failure("Invalid Username or Password!", Models.Enums.ApiStatusCode.RecordNotFound));
}
else
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, model.Username)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(new ClaimsPrincipal(claimsIdentity));
return Redirect(model.ReturnUrl);
}
}
}
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
//options.LoginPath = "/login";
options.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = (context) =>
{
context.HttpContext.Response.Redirect("http://localhost:4200/login" + context.RedirectUri.Split("Login")[1]);
return Task.CompletedTask;
}
};
});

CORS issue with one POST request but not another

I've looked at a few other questions, and they aren't having quite the same problem I'm having. I have a server running at localhost:5001 and a Vue website running at localhost:8080. When I do a /inventory/login post request, I'm not getting any CORS issues, but when I do a /inventory/computers request, I'm getting the following
Access to XMLHttpRequest at 'https://localhost:5001/inventory/computers' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
What am I doing wrong? Here's the relevant code.
Startup.cs:
public void ConfigureServices(IServiceCollection services) {
var connection = Configuration.GetConnectionString("Inventory");
services.AddDbContextPool<InventoryContext>(options => options.UseSqlServer(connection));
services.AddControllers().AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => {
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters() {
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["Jwt:Audience"],
ValidIssuer = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddCors(options => {
options.AddPolicy("AllowSpecificOrigin", builder => {
builder.WithOrigins("localhost")
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
}
LoginController:
[Route("inventory/[controller]")]
[ApiController]
[EnableCors("AllowSpecificOrigin")]
public class LoginController : ControllerBase {
public IConfiguration _configuration;
private readonly InventoryContext _context;
public LoginController(IConfiguration config, InventoryContext context) {
_configuration = config;
_context = context;
}
[HttpPost]
public async Task<IActionResult> Post(LoginRequest req) {
if (req != null && req.Username != null && req.Password != null) {
User user = await _context.Users.FirstOrDefaultAsync(u => u.Username == req.Username);
if (user == null || !Crypto.VerifyHashedPassword(user.HashedPassword, req.Password)) {
return BadRequest("Invalid username or password.");
}
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, _configuration["Jwt:Subject"]),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
new Claim("EmployeeID", user.EmployeeId.ToString()),
new Claim("Username", user.Username),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_configuration["Jwt:Issuer"], _configuration["Jwt:Audience"], claims, expires: DateTime.UtcNow.AddDays(1), signingCredentials: signIn);
return Ok(new JwtSecurityTokenHandler().WriteToken(token));
}
else {
return BadRequest();
}
}
ComputerController:
[Authorize]
[Route("inventory/[controller]")]
[ApiController]
[EnableCors("AllowSpecificOrigin")]
public class ComputersController : ControllerBase {
private readonly InventoryContext _context;
public ComputersController(InventoryContext context) {
_context = context;
}
// POST: api/Computers
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
[EnableCors("AllowSpecificOrigin")]
[HttpPost]
public async Task<ActionResult<Computer>> PostComputer(ComputerPostRequest req) {
Item item = new Item("Computer");
_context.Items.Add(item);
await _context.SaveChangesAsync();
Computer computer = new Computer(item.ItemId, req.SelectBankcardSerialNumber, req.ManufacturerSerialNumber, req.Model, req.Name, req.Value, req.EmployeeID, req.DateObtained);
_context.Computers.Add(computer);
if (!String.IsNullOrEmpty(req.Notes)) {
Note note = new Note(computer.ComputerId, req.Notes, DateTime.Now);
_context.Notes.Add(note);
}
await _context.SaveChangesAsync();
return CreatedAtAction("GetComputer", new { id = computer.ComputerId }, computer);
}
And the Vue code:
Login.vue
async login() {
if (this.isEmpty(this.username) || this.isEmpty(this.password)) {
this.incorrect = true;
console.log("Something's wrong");
return false;
}
try {
let res = await axios.post('https://localhost:5001/inventory/login', {
username: this.username,
password: this.password
});
this.$root.$authToken = "Bearer " + res.data;
} catch (error) {
console.log(error);
}
}
Computers.vue:
async upload() {
if (this.isEmpty(this.sbSerialNum) || this.isEmpty(this.mSerialNum) || this.isEmpty(this.compModel) || this.isEmpty(this.compName) ||
this.value === 0.0 || this.empID === 0 || this.isEmpty(this.dateObtained) || this.isEmpty(this.notes)) {
this.incorrect = true;
console.log("Something's wrong");
return false;
}
try {
const options = {
headers: {'Authorization': this.$root.$authToken}
};
let res = await axios.post('https://localhost:5001/inventory/computers', {
SelectBankcardSerialNumber: this.sbSerialNum,
ManufacturerSerialNumber: this.mSerialNum,
Name: this.compName,
Model: this.compModel,
Value: this.value,
EmployeeID: this.empID,
DateObtained: this.dateObtained,
Notes: this.notes
}, options);
this.computerAdded = res.data;
} catch (error) {
console.log(error);
}
You can try to move AddCors to the top of ConfigureServices method and just for the test pourposes I would use this
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(o => o.AddPolicy("AllowSpecificOrigin", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
.....
}
only if it works, I would try to use specific origins.
You have to place UseCors between UseRouting and UseAutorization
app.UseRouting();
app.UseCors("AllowSpecificOrigin");
app.UseAuthorization();

How to redirect already authorized user back to the Previous page in identity server 4 .NET CORE?

Below is the code added in Auth Server and Client machine
If any more info needed on this please comment.
StartUp.cs -- Auth Server
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
/*.AddProfileService<ProfileService>()*/;
builder.AddDeveloperSigningCredential();
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
//services.AddTransient<IProfileService, ProfileService>();
}
StartUp.cs -- Client App
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:44356/";
options.ClientId = "TestIdpApp";
options.ResponseType = "code";
//options.UsePkce = false;
//options.CallbackPath = new PathString("...")
options.Scope.Add("openid");
options.Scope.Add("profile");
options.SaveTokens = true;
options.ClientSecret = "secret";
});
}
ResourceOwnerPasswordValidator.cs -- Auth Server
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
UserInfo user = await GetUserDetails(context.UserName, context.Password).ConfigureAwait(false);
if (user != null && string.IsNullOrEmpty(user.ErrorMessage))
{
//set the result
context.Result = new GrantValidationResult(
subject: user.UserId.ToString(),
authenticationMethod: "custom",
claims: GetUserClaims(user));
return;
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, user.ErrorMessage);
return;
}
catch (Exception ex)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password");
}
}
//build claims array from user data
public static Claim[] GetUserClaims(UserInfo user)
{
return new Claim[]
{
new Claim("user_id", user.UserId.ToString() ?? ""),
new Claim(JwtClaimTypes.Name, (!string.IsNullOrEmpty(user.FirstName) && !string.IsNullOrEmpty(user.Surname)) ? (user.FirstName + " " + user.Surname) : ""),
new Claim(JwtClaimTypes.GivenName, user.FirstName ?? ""),
new Claim(JwtClaimTypes.FamilyName, user.Surname ?? ""),
new Claim(JwtClaimTypes.Email, user.Email ?? "")
};
}
}
Login Logic ---
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (button == "reset")
{
return Redirect("ResetPasswordWithoutCode");
}
else
{
// the user clicked the "cancel" button
if (button == "cancel")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", model.ReturnUrl);
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
ResourceOwnerPasswordValidationContext context1 = new ResourceOwnerPasswordValidationContext();
context1.UserName = model.Username;
context1.Password = model.Password;
// validate username/password against in-memory store
await _resourceOwner.ValidateAsync(context1);
if (context1.Result.Subject!=null && context1.Result.Subject.Identity.IsAuthenticated)
{
var user = await ResourceOwnerPasswordValidator.GetUserDetails(model.Username,model.Password).ConfigureAwait(false);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.UserId, user.Username, clientId: context?.Client.ClientId));
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
// issue authentication cookie with subject ID and username
var isuser = new IdentityServerUser(user.UserId)
{
DisplayName = user.Username
};
await HttpContext.SignInAsync(isuser, props);
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", model.ReturnUrl);
}
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.Client.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}
}
I'm calling a webapi to check the credentials and return info about user like First name last name.
RedirectUri - https://localhost:44356/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3DTestIdpApp%26redirect_uri%3Dhttps%253A%252F%252Flocalhost%253A44335%252Fsignin-oidc%26response_type%3Dcode%26scope%3Dopenid%2520profile%26code_challenge%3DLEZaLWC8ShzJz6LGUsdeUPr974clsUSYVPXWDNmbwOE%26code_challenge_method%3DS256%26response_mode%3Dform_post%26nonce%3D637360236231259886.MmIxNjlhODMtZTJhYy00YzUzLTliYjMtZWJmNzM3ZjRiM2VlZmYwYzI2MDAtNWRjYS00NThlLWI4MjAtY2ViYjgxY2RlYmZi%26state%3DCfDJ8LFJDL-5o71Ao2KksnBDgPVrH1DIIiM9LZSGUG43HRwLS6OjGUiGPwZ_xxT1RVryTZh7z3zwezVbdiy1L94mFlWausuYQrDNTWtzxrpTf2CrKjHRjcUIyNt5tX_g-yZYkWvxzCiyrpxnp7cctbNGoCmj_kqidhxCWsZee_26c3eVqfJfH7XEDfKUMj2BHeKQe_Ar9f2SkZJ0SBuy6MBe6zpU7-DDOYotDn-oO5zrtaHL8GCZfSqckqalL5yaGeolZ1ZDcubY01InyrBh1NwlVQRdGZRRWIZ-WnqqFKrTboQyw4rQswR-7BaLTtL8QitRkUmwS17LBLUvXKRBs8C0NUsX9HyREnmCVG2qW6s2AVpnE4iSt4XVSRcY-crXml2FjA%26x-client-SKU%3DID_NETSTANDARD2_0%26x-client-ver%3D5.5.0.0
You could have something like this in your GET Login action:
if (this.User.Identity.IsAuthenticated)
{
return Redirect(returnUrl);
}

Identity server identity token is always null

I'm using identity server with .net core application.
Current scenario: Getting token from Facebook, authenticating the user using Facebook API. And then creating a user in our database.
Everything is working fine, except that the returned token always have null identity_token.
I've tried to add id_token token to the scopes but didn't work also.
Login API:
[HttpPost("SocialLogin")]
public async Task<IActionResult> SocialMediaLogin()
{
try
{
var protocol = _httpContextAccessor.HttpContext.Request.Scheme;
var host = _httpContextAccessor.HttpContext.Request.Host.Value;
var baseUrl = $"{protocol}://{host}/";
#region Validating Headers and Parameters
StringValues jwtToken = Request.Headers["token"];
if (!jwtToken.Any())
{
return BadRequest("Each user should have a token");
}
var secretKey = _configuration["SecretKey"].ToString();
var (status, paylod) = GeneralUtilities.CheckSocialMediaTokenHeaderAndValues(jwtToken, secretKey);
if (status != ResponseStatus.Success)
{
return BadRequest("User Id or token are wrong");
}
#endregion
var clientId = _configuration["Clients:Mobile:Id"].ToString();
var secret = _configuration["Clients:Mobile:Secret"].ToString();
var consumerKey = _configuration["ConsumerKey"].ToString();
var consumerKeySecret = _configuration["ConsumerKeySecret"].ToString();
var disco = await DiscoveryClient.GetAsync(baseUrl);
if (disco.IsError) throw new Exception(disco.Error);
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, secret);
var payload = new
{
provider = paylod.Provider,
external_token = paylod.Token,
email = paylod.Email,
profilePicture = paylod.ProfilePictureUrl,
twitter_secret = paylod.TwitterSecret,
consumerKeySecret,
consumerKey,
displayName = paylod.DisplayName
};
//If user exist, we should update the profile Url of the user
var user = await _userManager.FindByEmailAsync(payload.email);
if (user != null)
{
user.ProfilePictureUrl = payload.profilePicture;
await _userManager.UpdateAsync(user);
}
var result = await tokenClient.RequestCustomGrantAsync(grantType: "external", scope: "my-api offline_access", extra: payload);
if (result.IsError) return new JsonResult(result.Json);
if (!string.IsNullOrWhiteSpace(result.AccessToken))
{
return Ok(result);
}
return new JsonResult(null);
}
catch (Exception)
{
return BadRequest();
}
}
As for the Startup.cs here's what I'm doing for Identity:
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddIdentityServer()
.AddOperationalStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqloptions => sqloptions.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqloptions => sqloptions.MigrationsAssembly(migrationsAssembly)))
.AddAspNetIdentity<AppUser>()
.AddDeveloperSigningCredential();
// Configure Identity
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
});
Where does the identity_token or access_token are built?

Authentication & Authorization - Token in HTTP request body

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;
}
};
});