identity: fix session revokation

This commit is contained in:
Abdullah Atta
2023-10-28 11:08:17 +05:00
parent 3746c4b42b
commit 1f72e2c3a8
12 changed files with 100 additions and 50 deletions

View File

@@ -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" />

View File

@@ -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." })
}
});

View File

@@ -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();

View File

@@ -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

View File

@@ -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)

View 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; }
}
}

View File

@@ -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";
}
}

View File

@@ -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
{

View File

@@ -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
});
}

View File

@@ -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())
{

View File

@@ -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 =>

View File

@@ -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>