SignalR hub authorization.feature/314_online_status_for_user
| @@ -0,0 +1,24 @@ | |||
| using Diligent.WebAPI.Business.MongoServices; | |||
| using Diligent.WebAPI.Data; | |||
| using Diligent.WebAPI.Data.Entities; | |||
| using Microsoft.Extensions.Options; | |||
| using MongoDB.Driver; | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Diligent.WebAPI.Business.Services | |||
| { | |||
| public class AuthorizationService : BaseMongo<Customer> | |||
| { | |||
| public AuthorizationService(IOptions<WebApiDatabaseSettings> webApiDatabaseSettings) : | |||
| base(webApiDatabaseSettings, "Customer") | |||
| { } | |||
| public async Task<Customer> GetByUserName(string username) | |||
| { | |||
| return await _mongoCollection.Find(c => c.UserName == username).FirstOrDefaultAsync(); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,6 +1,7 @@ | |||
| using AutoMapper; | |||
| using Diligent.WebAPI.Business.Services; | |||
| using Diligent.WebAPI.Data.Entities; | |||
| using Microsoft.AspNetCore.Authorization; | |||
| using Microsoft.AspNetCore.Mvc; | |||
| namespace Diligent.WebAPI.Host.Controllers | |||
| @@ -24,6 +25,7 @@ namespace Diligent.WebAPI.Host.Controllers | |||
| } | |||
| [HttpPost] | |||
| [Authorize(Roles = "Customer")] | |||
| public async Task<IActionResult> CreateChat(Room room) | |||
| { | |||
| if(room == null) | |||
| @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Identity; | |||
| using Microsoft.IdentityModel.Tokens; | |||
| using System.Text; | |||
| using Diligent.WebAPI.Business.Services; | |||
| using Diligent.WebAPI.Host.Middlewares; | |||
| namespace Diligent.WebAPI.Host.Extensions; | |||
| @@ -40,9 +40,16 @@ public static class WebAppBuilder | |||
| builder.Services.AddSingleton<InsurerService>(); | |||
| builder.Services.AddSingleton<RoomService>(); | |||
| builder.Services.AddSingleton<RequestService>(); | |||
| builder.Services.AddSingleton<AuthorizationService>(); | |||
| builder.Services.AddMediatR(Assembly.GetExecutingAssembly()); | |||
| builder.Services.AddSignalR(); | |||
| builder.Services.AddSignalR(options => | |||
| { | |||
| options.EnableDetailedErrors = true; | |||
| options.AddFilter<AuthorizationHubFilter>(); | |||
| }); | |||
| builder.Services.AddSingleton<AuthorizationHubFilter>(); | |||
| var mongoDbSettings = configuration.GetSection("WebApiDB"); | |||
| @@ -7,6 +7,7 @@ using Diligent.WebAPI.Host.Mediator.Notifications.Commands; | |||
| using MediatR; | |||
| using Microsoft.AspNetCore.Authorization; | |||
| using Microsoft.AspNetCore.SignalR; | |||
| using Diligent.WebAPI.Host.Middlewares; | |||
| namespace Diligent.WebAPI.Host.Hubs | |||
| { | |||
| @@ -33,6 +34,7 @@ namespace Diligent.WebAPI.Host.Hubs | |||
| //} | |||
| [HubMethodName("SendMessageToGroup")] | |||
| [AuthorizationHubFilter(Roles = "Customer,Support")] | |||
| public async Task SendMessageToGroup(ChatMessage message) | |||
| { | |||
| // Find user's room and send message to everyone | |||
| @@ -55,6 +57,7 @@ namespace Diligent.WebAPI.Host.Hubs | |||
| } | |||
| [HubMethodName("JoinRoom")] | |||
| [AuthorizationHubFilter(Roles = "Customer,Support")] | |||
| public async Task JoinRoom(UserConnection userConnection) | |||
| { | |||
| // Check is user in requested room | |||
| @@ -75,6 +78,7 @@ namespace Diligent.WebAPI.Host.Hubs | |||
| } | |||
| [HubMethodName("LeaveRoom")] | |||
| [AuthorizationHubFilter(Roles = "Customer,Support")] | |||
| public async Task LeaveRoom(LeaveChatRoomDTO connection) | |||
| { | |||
| if (_connections.TryGetValue(connection.ConnId, out UserConnection room)) | |||
| @@ -0,0 +1,117 @@ | |||
| using Diligent.WebAPI.Business.Services; | |||
| using Microsoft.AspNetCore.SignalR; | |||
| using Microsoft.IdentityModel.Tokens; | |||
| using System.IdentityModel.Tokens.Jwt; | |||
| using System.Net; | |||
| using System.Text; | |||
| using System.Web.Http; | |||
| namespace Diligent.WebAPI.Host.Middlewares | |||
| { | |||
| [AttributeUsage(AttributeTargets.Method)] | |||
| public class AuthorizationHubFilter : Attribute, IHubFilter | |||
| { | |||
| public string Roles; | |||
| public async ValueTask<object> InvokeMethodAsync( | |||
| HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next) | |||
| { | |||
| Type type = typeof(AuthorizationHubFilter); | |||
| var arguments = Attribute.GetCustomAttributes(invocationContext.HubMethod).Where(k => k.TypeId.ToString() == type.ToString()).ToList(); | |||
| // there is no custom attributes, so the filter does not need to perform authorization | |||
| if (!arguments.Any()) | |||
| return await next(invocationContext); | |||
| // get filter attribute | |||
| var roles = Attribute.GetCustomAttribute(invocationContext.HubMethod, arguments[0].GetType()); | |||
| if (roles is null) | |||
| throw new NullReferenceException(); | |||
| // get roles from filter attribute and split roles into an array | |||
| var arrayOfRoles = ((AuthorizationHubFilter)roles).Roles.Split(","); | |||
| var context = invocationContext.Context.GetHttpContext(); | |||
| if (context is null) | |||
| throw new NullReferenceException(); | |||
| var token = context.Request.Query["access_token"]; | |||
| await AttachUserToContext(context, token, arrayOfRoles); | |||
| return await next(invocationContext); | |||
| } | |||
| private static async Task AttachUserToContext(HttpContext context, string token, string[] roles) | |||
| { | |||
| bool contain = false; | |||
| try | |||
| { | |||
| var authorizationService = context.RequestServices.GetService(typeof(AuthorizationService)); | |||
| var configuration = context.RequestServices.GetService(typeof(IConfiguration)); | |||
| if (authorizationService is null || configuration is null) | |||
| throw new NullReferenceException(); | |||
| var tokenHandler = new JwtSecurityTokenHandler(); | |||
| var jwtSettings = ((IConfiguration)configuration).GetSection("JwtSettings"); | |||
| var issuer = jwtSettings["validIssuer"]; | |||
| var audience = jwtSettings["validAudience"]; | |||
| var key = Encoding.UTF8.GetBytes(jwtSettings["jwtSecret"]); | |||
| tokenHandler.ValidateToken(token, new TokenValidationParameters | |||
| { | |||
| ValidateIssuerSigningKey = true, | |||
| IssuerSigningKey = new SymmetricSecurityKey(key), | |||
| ValidIssuer = issuer, | |||
| ValidAudience = audience, | |||
| ValidateIssuer = true, | |||
| ValidateAudience = true, | |||
| ValidateLifetime = true, | |||
| }, out SecurityToken validatedToken); | |||
| var jwtToken = (JwtSecurityToken)validatedToken; | |||
| var userName = jwtToken.Payload["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"].ToString(); | |||
| var rl = (jwtToken.Payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]).ToString(); | |||
| if (rl is null) | |||
| throw new Exception(); | |||
| var rlArray = rl.Split(","); | |||
| //check is there any role from token same as role from attribute of authorization filter | |||
| foreach (var t in rlArray) | |||
| { | |||
| if (roles.Contains(t)) | |||
| { | |||
| contain = true; | |||
| break; | |||
| } | |||
| } | |||
| if (!contain) | |||
| { | |||
| throw new Exception(); | |||
| } | |||
| // attach user to context on successful jwt validation | |||
| if (userName != null) | |||
| { | |||
| context.Items["User"] = await ((AuthorizationService)authorizationService).GetByUserName(userName); | |||
| } | |||
| } | |||
| catch | |||
| { | |||
| // user don't have appropriate role to access the hub method | |||
| if (!contain) | |||
| { | |||
| throw new HttpResponseException(HttpStatusCode.Forbidden); | |||
| } | |||
| throw new UnauthorizedAccessException(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -40,7 +40,6 @@ const ChatList = () => { | |||
| useEffect(() => { | |||
| if (user && !loadedNotification) { | |||
| console.log("Ovde"); | |||
| dispatch(loadNotifications(user.id)); | |||
| setLoadedNotification((oldState) => !oldState); | |||
| } | |||
| @@ -77,7 +76,7 @@ const ChatList = () => { | |||
| const joinRoom = async (n) => { | |||
| try { | |||
| const connection = new HubConnectionBuilder() | |||
| .withUrl("http://localhost:5116/chatHub") | |||
| .withUrl("http://localhost:5116/chatHub",{accessTokenFactory:() => user.token}) | |||
| .withAutomaticReconnect() | |||
| .build(); | |||
| @@ -1,5 +1,6 @@ | |||
| import { createContext, useEffect, useState } from "react"; | |||
| import { useNavigate } from "react-router-dom"; | |||
| import axios from "axios"; | |||
| export const UserContext = createContext(); | |||
| @@ -7,9 +8,9 @@ export const UserProvider = (props) => { | |||
| const [user, setUser] = useState(null); | |||
| const navigate = useNavigate(); | |||
| // if (JSON.parse(localStorage.getItem('user'))) { | |||
| // axios.defaults.headers.common['Authorization'] = `Bearer ${JSON.parse(localStorage.getItem('user')).token}`; | |||
| // } | |||
| if (JSON.parse(localStorage.getItem('user'))) { | |||
| axios.defaults.headers.common['Authorization'] = `Bearer ${JSON.parse(localStorage.getItem('user')).token}`; | |||
| } | |||
| useEffect(() => { | |||
| if (localStorage.getItem("user") !== undefined) { | |||
| @@ -133,7 +133,6 @@ const chatSlice = createSlice({ | |||
| (notification) => notification.roomId !== action.payload | |||
| ); | |||
| }, | |||
| leaveRoom: (state, action) => { | |||
| state.activeRoom = null; | |||