mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 11:12:44 +00:00
identity: fix session revokation
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.Core" Version="3.7.12.5" />
|
||||
<PackageReference Include="DotNetEnv" Version="2.3.0" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="6.0.1-rc2.2" />
|
||||
|
||||
@@ -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<UserService>.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." })
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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<IUserService>();
|
||||
realm.Subscribe<DeleteUserMessage>(server.Topics.DeleteUserTopic, async (ev) =>
|
||||
|
||||
realm.Subscribe<DeleteUserMessage>(IdentityServerTopics.DeleteUserTopic, async (ev) =>
|
||||
{
|
||||
await service.DeleteUserAsync(ev.UserId, null);
|
||||
});
|
||||
|
||||
IDistributedCache cache = app.GetScopedService<IDistributedCache>();
|
||||
realm.Subscribe<ClearCacheMessage>(IdentityServerTopics.ClearCacheTopic, (ev) => ev.Keys.ForEach((key) => cache.Remove(key)));
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
38
Streetwriters.Common/Messages/ClearCacheMessage.cs
Normal file
38
Streetwriters.Common/Messages/ClearCacheMessage.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<string> keys)
|
||||
{
|
||||
this.Keys = keys;
|
||||
}
|
||||
|
||||
[JsonPropertyName("keys")]
|
||||
public List<string> Keys { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Models;
|
||||
using Streetwriters.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Streetwriters.Identity
|
||||
{
|
||||
|
||||
@@ -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<string>();
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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<IServerSentEventsService>();
|
||||
realm.Subscribe<SendSSEMessage>(server.Topics.SendSSETopic, async (ev) =>
|
||||
realm.Subscribe<SendSSEMessage>(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<IDistributedCache>();
|
||||
realm.Subscribe<ClearCacheMessage>(IdentityServerTopics.ClearCacheTopic, (ev) => ev.Keys.ForEach((key) => cache.Remove(key)));
|
||||
});
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<PackageReference Include="Lib.AspNetCore.ServerSentEvents" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.0" NoWarn="NU1605" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.0" NoWarn="NU1605" />
|
||||
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
|
||||
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user