Custom claims do not persist outside of middleware - c#

I am trying to create custom claims and persist them through the session. Unfortunately, it looks like they don't persist outside of the middleware itself. Am I missing something?
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using SRD.Data;
using SRD.Extensions;
namespace SRD.Security
{
public class ServerClaimsMiddleware
{
private readonly RequestDelegate _next;
public ServerClaimsMiddleware(RequestDelegate next)
{
_next = next;
}
private const string ClaimsLastCheckedKey = "ClaimsLastChecked";
public async Task Invoke(HttpContext context, DbContext dbContext, IDistributedCache cache)
{
if (context.User.Identity.IsAuthenticated)
{
var claimsLastChecked = await cache.RetrieveFromCache<DateTimeOffset?>(ClaimsLastCheckedKey);
if (!claimsLastChecked.HasValue)
{
var cs = new List<Claim>();
using (var principalContext = new PrincipalContext(ContextType.Domain))
{
if (context.User.Identity is ClaimsIdentity identity)
{
var user = UserPrincipal.FindByIdentity(principalContext, identity.Name);
if (user != null) cs.Add(new Claim(CustomClaimType.DisplayName.ToString(), user.DisplayName));
}
}
var roles = await dbContext.Roles.All();
foreach (var role in roles.Where(r => context.User.IsInRole(r.ADGroupName)))
{
var roleClaims = dbContext.Claims.ByRole(role.ADGroupName);
var customClaims = roleClaims.Select(x => new Claim(CustomClaimType.Permission.ToString(), x.Name));
cs.AddRange(customClaims);
}
if (cs.Any()) context.User.AddIdentity(new ClaimsIdentity(cs, "Kerbros"));
await cache.SaveToCache(ClaimsLastCheckedKey, DateTimeOffset.Now, new TimeSpan(0, 0, 15, 0));
}
}
await _next(context);
}
}
public static class ServerClaimsMiddlewareExtensions
{
public static IApplicationBuilder UseServerClaimsMiddleware(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<ServerClaimsMiddleware>();
}
}
}

You are adding them to the ClaimsPrincipal, but that doesn't persist the data anywhere.
When ASP.NET Core authenticates a request, it creates a ClaimsPrincipal from the cookie/token/something else.
It does not go the other way around automatically; modifying the principal is purely in-memory.
If your app is the one creating the cookie/token,
I think you can write a new one by calling context.SignInAsync() in your middleware after modifying the principal.
// you need this import
using Microsoft.AspNetCore.Authentication;
// In your middleware Invoke()
await context.SignInAsync(context.User);
You can also specify an authentication scheme to SignInAsync() if you have not configured a default sign-in scheme.

Related

How can I make SignInAsync working right here?

Can someone tell me how to fix that error?
HttpContext does not contain a definition of SignInAsync.
In another project I made with tutorial it works right. Using package: Microsoft.AspNetCore.Http;
public async Task<ActionResult<User>> LoginUser(LoginDto dto)
{
User loggedInUser = await _context.Users.Where(u => u.Email == dto.Email && u.PasswordHash == HashDtoPassword(dto.Password).PasswordHash).FirstOrDefaultAsync();
if (loggedInUser != null)
{
//create a claim
var claim = new Claim(ClaimTypes.Name, loggedInUser.Email);
//create claimsIdentity
var claimsIdentity = new ClaimsIdentity(new[] { claim }, "serverAuth");
//create claimsPrincipal
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
//Sign In User
//error down here
await HttpContext.SignInAsync(claimsPrincipal);
}
return await Task.FromResult(loggedInUser);
}
SigninAsync is an extension method of the class HttpContext in the Microsoft.AspNetCore.Authentication namespace. Here is a working example based off a new Web API project. You probably need to add using Microsoft.AspNetCore.Authentication;.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
namespace WebApplication3.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public async Task<ActionResult> GetAsync()
{
await HttpContext.SignInAsync("scheme", new System.Security.Claims.ClaimsPrincipal());
return Ok();
}
}
}

Openiddict: How to assign a list of permissions to OpenIddictApplicationDescriptor?

How to assign a list of permissions to OpenIddictApplicationDescriptor.Permissions?
OpenIddictApplicationDescriptor.Permissions has no set, it only has set?
https://github.com/openiddict/openiddict-samples/blob/dev/samples/Balosar/Balosar.Server/Worker.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using Balosar.Server.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Balosar.Server
{
public class Worker : IHostedService
{
private readonly IServiceProvider _serviceProvider;
public Worker(IServiceProvider serviceProvider)
=> _serviceProvider = serviceProvider;
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.EnsureCreatedAsync();
var manager = scope.ServiceProvider.GetRequiredService<OpenIddictApplicationManager<OpenIddictEntityFrameworkCoreApplication>>();
if (await manager.FindByClientIdAsync("balosar-blazor-client") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "balosar-blazor-client",
ConsentType = ConsentTypes.Explicit,
DisplayName = "Blazor client application",
Type = ClientTypes.Public,
PostLogoutRedirectUris =
{
new Uri("https://localhost:44310/authentication/logout-callback")
},
RedirectUris =
{
new Uri("https://localhost:44310/authentication/login-callback")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
OpenIddictApplicationDescriptor.Permissions has no set, it only has set?
It's a mutable collection: you can call Add on it to add the permissions you need.

Problem with the pipeline request. System just drops out and doesn't proceed

I have a method that is a custom middleware. This was copied exactly from the last solution with no changes.
using JobsLedger.AUTHORISATION.API.SessionMiddleware.Interfaces;
using JobsLedger.INTERFACES;
using Microsoft.AspNetCore.Http;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace JobsLedger.AUTHORISATION.API.SessionMiddleware
{
public class ConfigureSessionMiddleware
{
private readonly RequestDelegate _next;
public ConfigureSessionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext, IUserSession userSession, ISessionServices sessionServices)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (userSession == null)
{
throw new ArgumentNullException(nameof(userSession));
}
if (sessionServices == null)
{
throw new ArgumentNullException(nameof(sessionServices));
}
if (httpContext.User.Identities.Any(id => id.IsAuthenticated))
{
if (httpContext.Session.GetString("connectionString") == null) // Session needs to be set..
{
userSession.UserId = httpContext.User.Claims.FirstOrDefault(x => x.Type == "userId")?.Value;
userSession.ConnectionString = sessionServices.ConnectionStringFromUserId(userSession.UserId);
httpContext.Session.SetString("userId", userSession.UserId);
httpContext.Session.SetString("connectionString", userSession.ConnectionString);
}
else // Session set so all we need to is to build userSession for data access..
{
userSession.UserId = httpContext.Session.GetString("userId");
userSession.ConnectionString = httpContext.Session.GetString("connectionString");
}
}
// Call the next delegate/middleware in the pipeline
await _next.Invoke(httpContext).ConfigureAwait(false);
}
}
}
You will note that the bottom or last line has await _next.Invoke(httpContext).ConfigureAwait(false); which should call the next middleware. It doent. Sends me straight back to localhost on the browser. I suspect its suppose to process the request but it just jumps out of the pipeline and I have no idea.
Why is it dumping me out?
How can I debug it?
Can I see the next item that is suppose to come in the pipeline? (even if its not coming up).
UPDATE
I have a former solution that I refactored the current one from. I got this running in 3.1 and checked among other things the request and compared it to the current solutions httpcontext request. Here it is - only difference is the new or refacted solution has https (top one is current one that is jumping out of the request pipeline. Would this make a difference? Would it be that it cant find a controller.
Here is the controller I am trying to reach.
using JobsLedger.API.ViewModels.Auth;
using JobsLedger.AUTHORISATION;
using JobsLedger.AUTHORISATION.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
namespace JobsLedger.API.Controllers.Authorisation {
[Route("api/[controller]")]
public class JwtController : Controller {
#region Variables
private readonly IUserAuthorisationServices _tokenService;
private readonly IOptions<JwtIssuerOptions> jwtOptions;
private readonly ILogger logger;
private readonly JsonSerializerSettings _serializerSettings;
#endregion
public JwtController(IUserAuthorisationServices tokenService,
IOptions<JwtIssuerOptions> jwtOptions,
ILoggerFactory loggerFactory) {
if (loggerFactory is null) throw new ArgumentNullException(nameof(loggerFactory));
_tokenService = tokenService ?? throw new ArgumentNullException(nameof(tokenService));
jwtOptions = jwtOptions ?? throw new ArgumentNullException(nameof(jwtOptions));
logger = loggerFactory.CreateLogger<JwtController>();
_serializerSettings = new JsonSerializerSettings {
Formatting = Formatting.Indented
};
//loggingRepository = _errorRepository;
//ThrowIfInvalidOptions(this.jwtOptions);
}
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Get([FromBody] LoginViewModel model)
{
if (model is null) throw new ArgumentNullException(nameof(model));
var userContext = _tokenService.ValidateUser(model.Email, model.Password);
if (userContext.Principal == null) {
logger.LogInformation($"Invalid username ({model.Email}) or password ({model.Password})");
return BadRequest("Invalid credentials");
}
I've got a feeling you've dependency injected in the wrong place. You need to inject your dependencies in the constructor and then just have HttpContext on InvokeAsync. So it's probably throwing an exception when the dependency is null.
private readonly RequestDelegate _next;
private readonly IUserSession _userSession;
private readonly ISessionServices _sessionServices;
public ConfigureSessionMiddleware(RequestDelegate next, IUserSession userSession, ISessionServices sessionServices)
{
_next = next;
_userSession = userSession;
_sessionServices = sessionServices;
}
public async Task InvokeAsync(HttpContext httpContext)
{
//rest of your code
}
If you're getting a 500 error, then you could put something like this at the start of Configure in Startup.cs which will should tell you the error in development.
app.UseExceptionHandler(options =>
{
options.Run(async context =>
{
var ex = context.Features.Get<IExceptionHandlerPathFeature>();
if (ex?.Error != null)
{
Debugger.Break();
}
});
});

How to add Web API authentication filter on Controller?

I implemented web api 2, authentication filter, based in this link https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/authentication-filters.
The filter works, but I can't apply in on Controller? I can only apply it globally like this;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new MyAuthenticationFilter()); // Global level
MyAuthenticationFilter implementation
using Test1.Web.Areas.Api.Models;
using Test1.Web.Areas.Api.Provisioning;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
public class MyAuthenticationFilter : IAuthenticationFilter
{
private static CustomerService = new CustomerService();
public bool AllowMultiple => true;
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
this.SetContextErrorResult(context);
return;
}
string apiKey = authorization.Scheme;
// 3. If there are credentials, check Schema exists. Schema has tapiKey value.
// Authorization: apiKey
if (string.IsNullOrWhiteSpace(apiKey))
{
this.SetContextErrorResult(context);
return;
}
// 4. Validate tenant. Here we could use caching
CustomerModel customer = CustomerService.Find(apiKey);
if (customer == null)
{
this.SetContextErrorResult(context);
return;
}
// 5. Credentials ok, set principal
IPrincipal principal = new GenericPrincipal(new GenericIdentity(apiKey), new string[] { });
context.Principal = principal;
return;
}
public async Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
// currently we don't need authentication challenge
return;
}
private void SetContextErrorResult(HttpAuthenticationContext context)
{
context.ErrorResult = new AuthenticationFailedResponse();
}
}
public class AuthenticationFailedResponse : IHttpActionResult
{
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent(JsonConvert.SerializeObject(new ApiErrorModel()
{
Message = "Authentication failed",
Description = "Missing or incorrect credentials"
}), Encoding.UTF8, "application/json")
};
return response;
}
}
A colleague of mine found the solution:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthenticationFilter : FilterAttribute, IAuthenticationFilter
Upper code has to be added to MyAuthentication. And now we can use it on Controller:
[ApiExceptionFilter]
[RoutePrefix("api/provisioning/v0")]
[MyAuthenticationFilter]
public class ProvisioningController : ApiController
First remove following code from webconfig
config.Filters.Add(new MyAuthenticationFilter()); // Global level
Then add attrbute on controller. And first make sure you have added namespace of that auhthenticationfilter. and extend your class with ActionFilterAttribute and override method onactionexcecution.

The type 'SecurityTokenDescriptor' exists in both 'Microsoft.IdentityModel.Tokens, Version=5.6.0.0' and 'Microsoft.IdentityModel, Version=3.5.0.'

I can't seem to find a way to resolve this issue, it just wont compile. Please advise of any info I can provide to make my question clearer.
Here is the class that is generating the conflict - the error appears under new SecurityTokenDescriptor:
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using DatingApp.API.Data;
using DatingApp.API.Dtos;
using DatingApp.API.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
namespace DatingApp.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IAuthRepository _repo;
private readonly IConfiguration _config;
public AuthController(IAuthRepository repo, IConfiguration config)
{
_config = config;
_repo = repo;
}
[HttpPost("register")]
public async Task<IActionResult> Register(UserForRegisterDto userForRegisterDto)
{
userForRegisterDto.Username = userForRegisterDto.Username.ToLower();
if (await _repo.UserExists(userForRegisterDto.Username))
return BadRequest("Username already exists");
var userToCreate = new User
{
Username = userForRegisterDto.Username
};
var createdUser = await _repo.Register(userToCreate, userForRegisterDto.Password);
return StatusCode(201);
}
[HttpPost("login")]
public async Task<IActionResult> login(UserForLoginDto userForLoginDto)
{
var userFromRepo = await _repo.Login(userForLoginDto.Username.
ToLower(), userForLoginDto.Password);
if (userFromRepo == null)
return Unauthorized();
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userFromRepo.Id.ToString()),
new Claim(ClaimTypes.Name, userFromRepo.Username)
};
var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8
.GetBytes(_config.GetSection("AppSettings:Token").Value));
var creds = new Microsoft.IdentityModel.Tokens.SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return Ok(new {
token = tokenHandler.WriteToken(token)
});
}
}
}
Seems like you dont need Microsoft.IdentityModel in this class. Therefor you can fix the conflict by removing Microsoft.IdentityModel from the package referencing list.
Goto : [projectname].csproj
Remove this
<PackageReference Include="Microsoft.IdentityModel" Version="6.0.0"/>
and build the project.
if you need identity model again, add that reference and build the project.
Specify which namespace you want it to use
var tokenDescriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddDays(1),
SigningCredentials = creds
};

Categories