So I want to setup a User Middleware which works for SignalR Hubs and Controllers.
It works fine with normal requests but with signalr it gets called but doesnt add to context.
Is it even possible? If so how can i do it?
namespace PortalCore.Middleware
{
public class JwtMiddleware
{
private readonly RequestDelegate _next;
public JwtMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, AuthService authService)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null)
{
AttachUserToContext(context, authService, token);
}
await _next(context);
}
private async void AttachUserToContext(HttpContext context, AuthService authService, string token)
{
User user = null;
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey =
new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(authService.SecretKey)),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
user = await authService.GetUserByUid(jwtToken.Claims.FirstOrDefault()?.Value);
}
catch (Exception e)
{
}
context.Items["User"] = user;
}
}
}
if you want to check auth of signalR hub then you can do it with query string.you can send token with signalR client url.After take token from query string and set to context.
Hub Code:
[Authorize]
public class ChatHub : Hub
you can add token in Context :
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrWhiteSpace(accessToken) &&
(path.StartsWithSegments("/api/hubs/chatHub")))
{
context.Token = accessToken;
}
return Task.CompletedTask;
},
Related
I am trying to get groups from AzureAD by calling graph in the hub in the OnConnectedAsync() to add the current user to groups.
When calling my graph service from a controller it works just fine and returns me the data I want, but when calling it from inside the hub I get an error:
IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. No account or login hint was passed to the AcquireTokenSilent call.
Any idea?
Here is my graph service :
public class GraphService : IGraphService
{
private readonly ITokenAcquisition _tokenAcquisition;
private readonly IHttpClientFactory _httpClientFactory;
public GraphService(ITokenAcquisition tokenAcquisition,
IHttpClientFactory clientFactory)
{
_httpClientFactory = clientFactory;
_tokenAcquisition = tokenAcquisition;
}
public async Task<IEnumerable<Group>> GetUserGroupsAsync()
{
var graphClient = await GetGraphClient();
var memberShipCollection = await graphClient
.Me
.MemberOf
.Request()
.GetAsync();
return memberShipCollection
.OfType<Group>();
}
private async Task<GraphServiceClient> GetGraphClient()
{
var token = await _tokenAcquisition
.GetAccessTokenForUserAsync(new string[] { "https://graph.microsoft.com/.default" });
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri("https://graph.microsoft.com/v1.0");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var graphClient = new GraphServiceClient(client)
{
AuthenticationProvider = new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
})
};
graphClient.BaseUrl = "https://graph.microsoft.com/v1.0";
return graphClient;
}
}
The hub :
[Authorize]
public class NotificationHub : Hub
{
ILogger<NotificationHub> _logger;
private readonly IGraphService _graphService;
public NotificationHub(ILogger<NotificationHub> logger,
IGraphService graphService)
{
_logger = logger;
_graphService = graphService;
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.AddToGroupAsync(Context.ConnectionId, group.DisplayName);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
//var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.RemoveFromGroupAsync(Context.ConnectionId, group.DisplayName);
}
private async Task<IEnumerable<Group>> GetUserDynamicGroupsAsync()
{
var AllUserGroups = await _graphService.GetUserGroupsAsync();
return AllUserGroups.Where(g => g.DisplayName.Contains("ONE_"));
}
}
The part related to auth in my startup:
public static IServiceCollection AddInternalAuthentification(this IServiceCollection services, IConfiguration configuration)
{
services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
Func<MessageReceivedContext, Task> existingOnMessageReceivedHandler = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await existingOnMessageReceivedHandler(context);
StringValues accessToken = context.Request.Query["access_token"];
PathString path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/Notify"))
context.Token = accessToken;
};
});
return services;
}
I am using ASPNET core 5.0 for both front-end and back-end API. It worked perfectly on the local machine, but I deploy both the front-end and API application it always gives me audience validation failure. here is the code I am using.
"Jwt": {
"Issuer": "RestaurantPortal",
"Audience": "http://mansoor0786-001-site1.ctempurl.com/",
"Key": "ASAscethtCVdAQAAAAEAACcQAAAAEDhnGasldjaslkjdleEnGunGWR4Z79AvrtgIjYXhcWZx4OqpvWbsdsdsdSafcV/ZuPw25KbhKWhg1SIXXU2Ad7maaGAk******"
},
I have kept this in appSettings of both front end and API applications. Here is API startup code
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build();
});
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(config =>
{
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
};
});
Here is the validation I am doing on API end when user wants to login.
public bool ValidateToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = JwtConfiguration.JWTIssuer,
ValidAudience = JwtConfiguration.JWTAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey)),
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
}
catch (Exception)
{
return false;
}
return true;
}
Locally it works fine but when deploy these both applications it gives me an error and when I try to login it doesn't allow me to login into system. Here are the URL for both API and front-end application. This where I generate token
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfiguration.JWTKey));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: JwtConfiguration.JWTIssuer,
audience: JwtConfiguration.JWTAudience,
claims: claims,
expires: DateTime.Now.AddHours(24),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return tokenString;
}
In this the configuration gets information from appSettings.json
public static class JwtConfiguration
{
public static readonly string JWTIssuer = Utils._config["Jwt:Issuer"];
public static readonly string JWTAudience = Utils._config["Jwt:Audience"];
public static readonly string JWTKey = Utils._config["Jwt:Key"];
}
This is my response from when I log in the user
if (apiResponseModel != null && apiResponseModel.Data != null && apiResponseModel.Data.Status == 1)
{
var claims = new List<Claim>
{
new Claim(AuthKeys.AccessToken, apiResponseModel.Data.AccessToken),
new Claim(AuthKeys.RefreshToken, apiResponseModel.Data.RefreshToken)
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
if (apiResponseModel.Data.RoleName == UserRole.Roles.Customer.GetEnumDescription())
{
return RedirectToAction("index", "Home");
}
return RedirectToAction("index", "dashboard");
}
After that it redirected to dashboard index page where I wrote base controller and added attribute on top of basecontroller which does the following.
[ServiceFilter(typeof(JWT_Authentication))]
public class BaseController : Controller
{
public readonly IOptions<AppSettingDTO> _appSetting;
protected readonly IUserProfileInfo _userService;
public readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IHttpNetClientService _apiService;
public BaseController(IOptions<AppSettingDTO> AppSetting, IHttpNetClientService HttpService, IUserProfileInfo UserInfo, IHttpContextAccessor HttpContext)
{
_appSetting = AppSetting;
_apiService = HttpService;
_userService = UserInfo;
_httpContextAccessor = HttpContext;
}
}
Here is my JWT_Authentication
public class JWT_Authentication : ActionFilterAttribute
{
private readonly IHttpContextAccessor _httpContextAccessor;
protected readonly IUserProfileInfo _userService;
public JWT_Authentication(IHttpContextAccessor HttpContext, IUserProfileInfo UserInfo)
{
_httpContextAccessor = HttpContext;
_userService = UserInfo;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
string actionName = context.RouteData.Values["Action"].ToString().ToLower();
string controllerName = context.RouteData.Values["Controller"].ToString().ToLower();
if (
controllerName != "account" && actionName != "logout")
{
string accessTokens = _userService.GetToken(_httpContextAccessor);
if (!_userService.ValidateToken(accessTokens))
{
}
else
{
return;
}
context.Result = new RedirectToRouteResult(new RouteValueDictionary(){
{ "action", "LogOut" },
{ "controller", "Account" }
});
return;
}
}
}
API
http://mansoor00786-001-site1.gtempurl.com/
Front-End
http://mansoor0786-001-site1.ctempurl.com/
I am calling login API from the front-end application which is also in asp net core 5.0 but it doesn't log me into the dashboard because of validation failure and that is because of the audience.
Well, as far as i saw it, here is some points I spot
There won't ever be an exception was throw when calling ValidateToken
Cause we put it on try catch block, so where does it throw audience validation failure ? It cannot be during deployment cause catch block doesn't have logging support anywhere, therefore, it might just be your assumption. And behavior on production state should be always redirect to Login page after logout.
The way MVC project handle Jwt Token was cumbersome
As we handmade Jwt Token and validate them ourself, such thing as validation failure with the same setting (Issuer, audience,...) should not exists. If that was fine on the client, have faith and logging those setting out from production state.
And for current approach, We can validate Jwt token and restrict them from access our resource fine, but HttpContext.User object still be null, therefore Authorization process became mostly, unusable.
Instead, how about consider to write our own Authentication scheme ?
What might be the problem here ?
public class JWT_Authentication : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//... Some upper process
if (!_userService.ValidateToken(accessTokens))
{
// Doing something if the jwt invalid ?
}
else
{
return;
}
//... Some below process
}
}
If my block code idea was right, take a look at string accessTokens = _userService.GetToken(_httpContextAccessor);, log it out, as there might be a null here, due to you passing down a IHttpContextAccessor, which was singleton, not a HttpContext which scope for each request (localhost would be fine, cause we have only one client).
I am working on a API right now where i can send /api/login get a jwt token and use it further to do some stuff on that api with my client app later
thats my startup where i add the services :
services.AddScoped<IUserCoreController>(x => new UserCoreController(x.GetRequiredServic<TerraContext>(), x.GetRequiredService<IHashEngine>()));
services.AddScoped<ITokenFactory>(x => new TokenFactory(x.GetRequiredService<IConfiguration>(), x.GetService<JwtSettings>(), x.GetService<AppDataSettings>(),x.GetRequiredService<IUserCoreController>()));
With this method i try to access to my db _context and check if a user with that email exists and return true/false
public async Task<ResponseModel> Authenticate(string email, string password)
{
using (var db = _context)
{
//logic of my method
}
the code below shows the constructor for this class and how i inject the dbcontext and a hashengine
public class UserCoreController : IUserCoreController
{
TerraContext _context;
IHashEngine _hashEngine;
public UserCoreController(TerraContext context, IHashEngine engine)
{
_hashEngine = engine;
_context = context;
}
until now i get a true from this method after that i generate a jwt token with the class below
{
public class TokenFactory : ITokenFactory
{
IUserCoreController _userCore;
TerraContext _context;
IConfiguration _config;
JwtSettings _jwtSettings;
AppDataSettings _appDataSettings;
public TokenFactory(IConfiguration config, JwtSettings jwtsettings, AppDataSettings appDataSettings, IUserCoreController userCore)
{
_config = config;
_jwtSettings = jwtsettings;
_appDataSettings = appDataSettings;
_userCore = userCore;
}
public async Task<ResponseModel> Authenticate(LoginModel model)
{
var response = await _userCore.Authenticate(model.Email, model.Password);
return response;
}
public async Task<TokenResponse> GenerateJwtToken(string id)
{
var token = new TokenResponse();
var user = await _userCore.GetUser(id);
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>();
if (!String.IsNullOrEmpty(user.Firstname))
claims.Add(new Claim("firstname", user.Firstname));
if (!String.IsNullOrEmpty(user.Lastname))
claims.Add(new Claim("lastname", user.Lastname));
if (!String.IsNullOrEmpty(user.Username))
claims.Add(new Claim("userId", user.UserId));
if (!String.IsNullOrEmpty(user.Username))
claims.Add(new Claim("appId", _appDataSettings.ApplicationId));
if (!String.IsNullOrEmpty(user.Username))
claims.Add(new Claim("orgId", _appDataSettings.OrganisationId));
var jwt = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
expires: DateTime.Now.AddHours(3),
signingCredentials: credentials,
claims: claims
);
var tokstring = new JwtSecurityTokenHandler().WriteToken(jwt);
token.Token = tokstring;
token.Email = user.Email;
token.TokenId = Guid.NewGuid().ToString();
return token;
}
public async Task<bool> ValidateToken(string token)
{
await Task.Delay(100);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = GetValidationParameters();
SecurityToken validatedToken;
IPrincipal principal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return true;
}
public async Task<ClaimsPrincipal> GetUser(string token)
{
await Task.Delay(100);
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = GetValidationParameters();
SecurityToken validatedToken;
var principal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return principal;
}
private TokenValidationParameters GetValidationParameters()
{
return new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidAudience = _jwtSettings.Audience,
ValidIssuer = _jwtSettings.Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey)),
ClockSkew = TimeSpan.Zero
};
}
}
I get the Exception System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Should i just stop coding with a using() statement with my DbContext?
I'm trying to transform.NetCore 3.1 Code from Bearer Token implementation to Cookie-based implementation Also trying to make Role-based authorization work with existing code. Can you please help me to change this code? The below code shows currently how Bearer Token is retrieved and the next part shows how role-based authorization is implemented in code.
Here is the current Bearer Token implementation.
var key = Encoding.ASCII.GetBytes(Configuration["AppSettings:Secret"]);
var signingKey = new SymmetricSecurityKey(key);
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,
ValidateLifetime = true,
IssuerSigningKey = signingKey,
ValidateIssuer = false,
ValidateAudience = false
};
});
Following annotation currently used for Role-based Authorization -
[Authorize(Roles = "1")]
[Route("api/[controller]")]
[ApiController]
public class JobLogsController : ControllerBase
{
private readonly EtpRepoContext _context;
private IJobLogsRepository _jobLogsRepository;
private IConfiguration _configuration;
public JobLogsController(EtpRepoContext context, IJobLogsRepository jobLogsRepository, IConfiguration configuration)
{
_context = context;
_jobLogsRepository = jobLogsRepository;
_configuration = configuration;
}
// GET: api/JobLogs
[HttpGet]
public async Task<ActionResult<IEnumerable<JobLog>>> GetJobLog()
{
return await _context.JobLog.ToListAsync();
}
// GET: api/JobLogs/5
[HttpGet("{id}")]
[ProducesResponseType(typeof(JobDetail), 200)]
[ProducesResponseType(typeof(string), 400)]
public IActionResult FindById([FromRoute] String id)
{
string contentStr = "";
try
{
if(id.Length >= 10)
{
contentStr = _jobLogsRepository.GetLogById(id);
}
else
{
contentStr = _jobLogsRepository.GetFileById(id);
}
var content = Newtonsoft.Json.JsonConvert.SerializeObject(new { content = contentStr });
return Ok(content);
}
catch (Exception ex)
{
return StatusCode(500, "Internal server error");
}
}
This is how the Microsoft identity model is used to claim the token.
public class ClaimsTransformer : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
ClaimsIdentity claimsIdentity = (ClaimsIdentity)principal.Identity;
// flatten realm_access because Microsoft identity model doesn't support nested claims
// by map it to Microsoft identity model, because automatic JWT bearer token mapping already processed here
if (claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim((claim) => claim.Type == "identity"))
{
var realmAccessClaim = claimsIdentity.FindFirst((claim) => claim.Type == "identity");
dynamic realmAccessAsDict = JsonConvert.DeserializeObject<Object>(realmAccessClaim.Value);
string role = realmAccessAsDict.role.ToString();
claimsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", role));
//var role = realmAccessClaim.
//var realmAccessAsDict = JsonConvert.DeserializeObject<Object>(realmAccessClaim.Value);
/*if (realmAccessAsDict["role"] != null)
{
foreach (var role in realmAccessAsDict["role"])
{
claimsIdentity.AddClaim(new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", role));
}
}*/
}
return Task.FromResult(principal);
}
}
}
I am attempting to receive data from the server controller, stocks.
I get this error:
"System.InvalidOperationException: Unable to resolve service for type myBackEnd.Models.StockContext' while attempting to activate 'myBackEnd.Controllers.StockController'.
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService
(IServiceProvider sp, Type type, Type requiredBy, Boolean
isDefaultParameterRequired"
Here is my stocks controller code:
namespace myBackEnd.Controllers
{
[Route("api/stock")]
[Produces("application/json")]
public class StockController : ControllerBase
{
private readonly int fastEmaPeriod = 10;
private readonly IHttpClientFactory _httpClientFactory;
private readonly Models.StockContext _context;
public StockController(Models.StockContext context, IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_context = context;
}
// POST api/values
[HttpPost]
public async Task<IActionResult> Post([FromBody]Models.Stock stock)
{
_context.Stocks.Add(stock);
await _context.SaveChangesAsync();
return Ok(stock);
}
This is the startup.cs code:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(o => o.AddPolicy("MyPolicy", corsBuilder =>
{
corsBuilder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
services.AddDbContext<DataContext>(x => x.UseInMemoryDatabase("TestDb"));
services.AddHttpClient();
services.AddAutoMapper();
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = userService.GetById(userId);
if (user == null)
{
// return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// configure DI for application services
services.AddScoped<IUserService, UserService>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
This worked before I added the registration, login and
// configure DI for application services
services.AddScoped();
The problem was the DB context was not registered for dependency injection.
Adding:
services.AddDbContext<Models.StockContext>(opt => opt.UseInMemoryDatabase("item"));
fixed the problem.