From 1f72e2c3a867b6d00e93e7faa34c06b26dedf597 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 28 Oct 2023 11:08:17 +0500 Subject: [PATCH] identity: fix session revokation --- Notesnook.API/Notesnook.API.csproj | 2 +- Notesnook.API/Services/UserService.cs | 12 ++--- Notesnook.API/Startup.cs | 8 +++- Streetwriters.Common/Clients.cs | 2 +- .../Extensions/StringExtensions.cs | 12 ++--- .../Messages/ClearCacheMessage.cs | 38 +++++++++++++++ Streetwriters.Common/WampServers.cs | 11 ++--- Streetwriters.Identity/Config.cs | 2 - .../Controllers/AccountController.cs | 47 +++++++++++-------- Streetwriters.Identity/Startup.cs | 4 +- Streetwriters.Messenger/Startup.cs | 10 ++-- .../Streetwriters.Messenger.csproj | 2 +- 12 files changed, 100 insertions(+), 50 deletions(-) create mode 100644 Streetwriters.Common/Messages/ClearCacheMessage.cs diff --git a/Notesnook.API/Notesnook.API.csproj b/Notesnook.API/Notesnook.API.csproj index 31d6f73..57b4b67 100644 --- a/Notesnook.API/Notesnook.API.csproj +++ b/Notesnook.API/Notesnook.API.csproj @@ -11,7 +11,7 @@ - + diff --git a/Notesnook.API/Services/UserService.cs b/Notesnook.API/Services/UserService.cs index cf3ed9e..a255fdb 100644 --- a/Notesnook.API/Services/UserService.cs +++ b/Notesnook.API/Services/UserService.cs @@ -76,7 +76,7 @@ namespace Notesnook.API.Services if (!Constants.IS_SELF_HOSTED) { - await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.CreateSubscriptionTopic, new CreateSubscriptionMessage + await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionTopic, new CreateSubscriptionMessage { AppId = ApplicationType.NOTESNOOK, Provider = SubscriptionProvider.STREETWRITERS, @@ -115,7 +115,7 @@ namespace Notesnook.API.Services { await Slogger.Error(nameof(GetUserAsync), "Repairing user subscription.", JsonSerializer.Serialize(response)); // user was partially created. We should continue the process here. - await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.CreateSubscriptionTopic, new CreateSubscriptionMessage + await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.CreateSubscriptionTopic, new CreateSubscriptionMessage { AppId = ApplicationType.NOTESNOOK, Provider = SubscriptionProvider.STREETWRITERS, @@ -182,22 +182,22 @@ namespace Notesnook.API.Services if (!Constants.IS_SELF_HOSTED) { - await WampServers.SubscriptionServer.PublishMessageAsync(WampServers.SubscriptionServer.Topics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage + await WampServers.SubscriptionServer.PublishMessageAsync(SubscriptionServerTopics.DeleteSubscriptionTopic, new DeleteSubscriptionMessage { AppId = ApplicationType.NOTESNOOK, UserId = userId }); } - await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage + await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage { SendToAll = false, OriginTokenId = jti, UserId = userId, Message = new Message { - Type = "userDeleted", - Data = JsonSerializer.Serialize(new { reason = "accountDeleted" }) + Type = "logout", + Data = JsonSerializer.Serialize(new { reason = "Account deleted." }) } }); diff --git a/Notesnook.API/Startup.cs b/Notesnook.API/Startup.cs index cbb96f4..0a055a7 100644 --- a/Notesnook.API/Startup.cs +++ b/Notesnook.API/Startup.cs @@ -34,6 +34,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -152,6 +153,7 @@ namespace Notesnook.API context.HttpContext.User = context.Principal; return Task.CompletedTask; }; + options.CacheKeyGenerator = (options, token) => (token + ":" + "reference_token").Sha256(); options.SaveToken = true; options.EnableCaching = true; options.CacheDuration = TimeSpan.FromMinutes(30); @@ -244,10 +246,14 @@ namespace Notesnook.API app.UseWamp(WampServers.NotesnookServer, (realm, server) => { IUserService service = app.GetScopedService(); - realm.Subscribe(server.Topics.DeleteUserTopic, async (ev) => + + realm.Subscribe(IdentityServerTopics.DeleteUserTopic, async (ev) => { await service.DeleteUserAsync(ev.UserId, null); }); + + IDistributedCache cache = app.GetScopedService(); + realm.Subscribe(IdentityServerTopics.ClearCacheTopic, (ev) => ev.Keys.ForEach((key) => cache.Remove(key))); }); app.UseRouting(); diff --git a/Streetwriters.Common/Clients.cs b/Streetwriters.Common/Clients.cs index 1718464..bd65a10 100644 --- a/Streetwriters.Common/Clients.cs +++ b/Streetwriters.Common/Clients.cs @@ -41,7 +41,7 @@ namespace Streetwriters.Common EmailConfirmedRedirectURL = $"{Constants.NOTESNOOK_APP_HOST}/account/verified", OnEmailConfirmed = async (userId) => { - await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage + await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage { UserId = userId, Message = new Message diff --git a/Streetwriters.Common/Extensions/StringExtensions.cs b/Streetwriters.Common/Extensions/StringExtensions.cs index 1a9e796..28b0f17 100644 --- a/Streetwriters.Common/Extensions/StringExtensions.cs +++ b/Streetwriters.Common/Extensions/StringExtensions.cs @@ -26,15 +26,11 @@ namespace System { public static class StringExtensions { - public static string ToSha256(this string rawData, int maxLength = 12) + public static string Sha256(this string input) { - // Create a SHA256 - using (SHA256 sha256Hash = SHA256.Create()) - { - // ComputeHash - returns byte array - byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData)); - return ToHex(bytes, 0, maxLength); - } + var bytes = Encoding.UTF8.GetBytes(input); + var hash = SHA256.HashData(bytes); + return Convert.ToBase64String(hash); } public static byte[] CompressBrotli(this string input) diff --git a/Streetwriters.Common/Messages/ClearCacheMessage.cs b/Streetwriters.Common/Messages/ClearCacheMessage.cs new file mode 100644 index 0000000..c06105d --- /dev/null +++ b/Streetwriters.Common/Messages/ClearCacheMessage.cs @@ -0,0 +1,38 @@ +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Streetwriters.Common.Enums; +using Streetwriters.Common.Interfaces; + +namespace Streetwriters.Common.Messages +{ + public class ClearCacheMessage + { + public ClearCacheMessage(List keys) + { + this.Keys = keys; + } + + [JsonPropertyName("keys")] + public List Keys { get; set; } + } +} \ No newline at end of file diff --git a/Streetwriters.Common/WampServers.cs b/Streetwriters.Common/WampServers.cs index bcf6bb3..d275430 100644 --- a/Streetwriters.Common/WampServers.cs +++ b/Streetwriters.Common/WampServers.cs @@ -97,23 +97,22 @@ namespace Streetwriters.Common public class MessengerServerTopics { - public string SendSSETopic => "com.streetwriters.sse.send"; + public const string SendSSETopic = "com.streetwriters.sse.send"; } public class SubscriptionServerTopics { - public string CreateSubscriptionTopic => "com.streetwriters.subscriptions.create"; - public string DeleteSubscriptionTopic => "com.streetwriters.subscriptions.delete"; + public const string CreateSubscriptionTopic = "com.streetwriters.subscriptions.create"; + public const string DeleteSubscriptionTopic = "com.streetwriters.subscriptions.delete"; } public class IdentityServerTopics { - public string CreateSubscriptionTopic => "com.streetwriters.subscriptions.create"; - public string DeleteSubscriptionTopic => "com.streetwriters.subscriptions.delete"; + public const string ClearCacheTopic = "com.streetwriters.identity.clear_cache"; + public const string DeleteUserTopic = "com.streetwriters.identity.delete_user"; } public class NotesnookServerTopics { - public string DeleteUserTopic => "com.streetwriters.notesnook.user.delete"; } } \ No newline at end of file diff --git a/Streetwriters.Identity/Config.cs b/Streetwriters.Identity/Config.cs index 8754480..2f09272 100644 --- a/Streetwriters.Identity/Config.cs +++ b/Streetwriters.Identity/Config.cs @@ -20,9 +20,7 @@ along with this program. If not, see . using IdentityServer4; using IdentityServer4.Models; using Streetwriters.Common; -using System; using System.Collections.Generic; -using System.Linq; namespace Streetwriters.Identity { diff --git a/Streetwriters.Identity/Controllers/AccountController.cs b/Streetwriters.Identity/Controllers/AccountController.cs index ffbc9c0..e66316d 100644 --- a/Streetwriters.Identity/Controllers/AccountController.cs +++ b/Streetwriters.Identity/Controllers/AccountController.cs @@ -21,9 +21,12 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Security.Claims; +using System.Text.Json; using System.Threading.Tasks; using AspNetCore.Identity.Mongo.Model; +using IdentityServer4; using IdentityServer4.Configuration; +using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -280,7 +283,7 @@ namespace Streetwriters.Identity.Controllers if (result.Succeeded) { await UserManager.SetUserNameAsync(user, form.NewEmail); - await SendEmailChangedMessageAsync(user.Id.ToString()); + await SendLogoutMessageAsync(user.Id.ToString(), "Email changed."); return Ok(); } } @@ -292,7 +295,7 @@ namespace Streetwriters.Identity.Controllers var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword); if (result.Succeeded) { - await SendPasswordChangedMessageAsync(user.Id.ToString()); + await SendLogoutMessageAsync(user.Id.ToString(), "Password changed."); return Ok(); } return BadRequest(result.Errors.ToErrors()); @@ -306,7 +309,7 @@ namespace Streetwriters.Identity.Controllers result = await UserManager.AddPasswordAsync(user, form.NewPassword); if (result.Succeeded) { - await SendPasswordChangedMessageAsync(user.Id.ToString()); + await SendLogoutMessageAsync(user.Id.ToString(), "Password reset."); return Ok(); } } @@ -334,7 +337,7 @@ namespace Streetwriters.Identity.Controllers if (client == null) return BadRequest("Invalid client_id."); var user = await UserManager.GetUserAsync(User); - if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id.ToString()}'."); + if (!await IsUserValidAsync(user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'."); var jti = User.FindFirstValue("jti"); @@ -343,37 +346,43 @@ namespace Streetwriters.Identity.Controllers ClientId = client.Id, SubjectId = user.Id.ToString() }); + var refreshTokenKey = GetHashedKey(refresh_token, PersistedGrantTypes.RefreshToken); + var removedKeys = new List(); foreach (var grant in grants) { - if (!all && (grant.Data.Contains(jti) || grant.Data.Contains(refresh_token))) continue; + if (!all && (grant.Data.Contains(jti) || grant.Key == refreshTokenKey)) continue; await PersistedGrantStore.RemoveAsync(grant.Key); + removedKeys.Add(grant.Key); } + + await WampServers.NotesnookServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); + await WampServers.MessengerServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); + await WampServers.SubscriptionServer.PublishMessageAsync(IdentityServerTopics.ClearCacheTopic, new ClearCacheMessage(removedKeys)); + await SendLogoutMessageAsync(user.Id.ToString(), "Session revoked."); return Ok(); } - private async Task SendPasswordChangedMessageAsync(string userId) + private static string GetHashedKey(string value, string grantType) { - await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage + return (value + ":" + grantType).Sha256(); + } + + private async Task SendLogoutMessageAsync(string userId, string reason) + { + await SendMessageAsync(userId, new Message { - UserId = userId, - OriginTokenId = User.FindFirstValue("jti"), - Message = new Message - { - Type = "userPasswordChanged" - } + Type = "logout", + Data = JsonSerializer.Serialize(new { reason }) }); } - private async Task SendEmailChangedMessageAsync(string userId) + private async Task SendMessageAsync(string userId, Message message) { - await WampServers.MessengerServer.PublishMessageAsync(WampServers.MessengerServer.Topics.SendSSETopic, new SendSSEMessage + await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage { UserId = userId, OriginTokenId = User.FindFirstValue("jti"), - Message = new Message - { - Type = "userEmailChanged" - } + Message = message }); } diff --git a/Streetwriters.Identity/Startup.cs b/Streetwriters.Identity/Startup.cs index 961e231..e459251 100644 --- a/Streetwriters.Identity/Startup.cs +++ b/Streetwriters.Identity/Startup.cs @@ -201,7 +201,7 @@ namespace Streetwriters.Identity app.UseWamp(WampServers.IdentityServer, (realm, server) => { - realm.Subscribe(server.Topics.CreateSubscriptionTopic, async (CreateSubscriptionMessage message) => + realm.Subscribe(SubscriptionServerTopics.CreateSubscriptionTopic, async (CreateSubscriptionMessage message) => { using (var serviceScope = app.ApplicationServices.CreateScope()) { @@ -210,7 +210,7 @@ namespace Streetwriters.Identity await MessageHandlers.CreateSubscription.Process(message, userManager); } }); - realm.Subscribe(server.Topics.DeleteSubscriptionTopic, async (DeleteSubscriptionMessage message) => + realm.Subscribe(SubscriptionServerTopics.DeleteSubscriptionTopic, async (DeleteSubscriptionMessage message) => { using (var serviceScope = app.ApplicationServices.CreateScope()) { diff --git a/Streetwriters.Messenger/Startup.cs b/Streetwriters.Messenger/Startup.cs index 5be2209..4bbff02 100644 --- a/Streetwriters.Messenger/Startup.cs +++ b/Streetwriters.Messenger/Startup.cs @@ -28,6 +28,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -74,11 +75,11 @@ namespace Streetwriters.Messenger options.Authority = Servers.IdentityServer.ToString(); options.ClientSecret = Constants.NOTESNOOK_API_SECRET; options.ClientId = "notesnook"; + options.DiscoveryPolicy.RequireHttps = false; + options.CacheKeyGenerator = (options, token) => (token + ":" + "reference_token").Sha256(); options.SaveToken = true; options.EnableCaching = true; options.CacheDuration = TimeSpan.FromMinutes(30); - // TODO - options.DiscoveryPolicy.RequireHttps = false; }); services.AddServerSentEvents(); @@ -119,7 +120,7 @@ namespace Streetwriters.Messenger app.UseWamp(WampServers.MessengerServer, (realm, server) => { IServerSentEventsService service = app.ApplicationServices.GetRequiredService(); - realm.Subscribe(server.Topics.SendSSETopic, async (ev) => + realm.Subscribe(MessengerServerTopics.SendSSETopic, async (ev) => { var message = JsonSerializer.Serialize(ev.Message); if (ev.SendToAll) @@ -131,6 +132,9 @@ namespace Streetwriters.Messenger await SSEHelper.SendEventToUserAsync(message, service, ev.UserId, ev.OriginTokenId); } }); + + IDistributedCache cache = app.GetScopedService(); + realm.Subscribe(IdentityServerTopics.ClearCacheTopic, (ev) => ev.Keys.ForEach((key) => cache.Remove(key))); }); app.UseEndpoints(endpoints => diff --git a/Streetwriters.Messenger/Streetwriters.Messenger.csproj b/Streetwriters.Messenger/Streetwriters.Messenger.csproj index 1799ef0..9d02d87 100644 --- a/Streetwriters.Messenger/Streetwriters.Messenger.csproj +++ b/Streetwriters.Messenger/Streetwriters.Messenger.csproj @@ -13,7 +13,7 @@ - +