diff --git a/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs b/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs index 2661298405..6f950524f3 100644 --- a/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs +++ b/framework/src/Volo.Abp.Security/System/Security/Principal/AbpClaimsIdentityExtensions.cs @@ -276,4 +276,37 @@ public static class AbpClaimsIdentityExtensions return principal; } + + public static Guid? FindSessionId([NotNull] this IIdentity identity) + { + Check.NotNull(identity, nameof(identity)); + + var claimsIdentity = identity as ClaimsIdentity; + + var sessionIdOrNull = claimsIdentity?.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.SessionId); + if (sessionIdOrNull == null || sessionIdOrNull.Value.IsNullOrWhiteSpace()) + { + return null; + } + + if (Guid.TryParse(sessionIdOrNull.Value, out var guid)) + { + return guid; + } + + return null; + } + + public static string? FindSessionId([NotNull] this ClaimsPrincipal principal) + { + Check.NotNull(principal, nameof(principal)); + + var sessionIdOrNull = principal.Claims?.FirstOrDefault(c => c.Type == AbpClaimTypes.SessionId); + if (sessionIdOrNull == null || sessionIdOrNull.Value.IsNullOrWhiteSpace()) + { + return null; + } + + return sessionIdOrNull.Value; + } } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs index 63628bd33a..52f721a4e6 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs @@ -87,9 +87,15 @@ public static class AbpClaimTypes /// Default: "impersonator_username". /// public static string ImpersonatorUserName { get; set; } = "impersonator_username"; - + /// /// Default: "picture". /// public static string Picture { get; set; } = "picture"; + + /// + /// Default: "session_id". + /// + public static string SessionId { get; set; } = "session_id"; + } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUserExtensions.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUserExtensions.cs index b702bcffde..3007cc95c3 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUserExtensions.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUserExtensions.cs @@ -70,4 +70,26 @@ public static class CurrentUserExtensions { return currentUser.FindClaimValue(AbpClaimTypes.ImpersonatorUserName); } + + public static Guid GetSessionId([NotNull] this ICurrentUser currentUser) + { + var sessionId = currentUser.FindSessionId(); + Debug.Assert(sessionId != null, "sessionId != null"); + return sessionId!.Value; + } + + public static Guid? FindSessionId([NotNull] this ICurrentUser currentUser) + { + var sessionId = currentUser.FindClaimValue(AbpClaimTypes.SessionId); + if (sessionId.IsNullOrWhiteSpace()) + { + return null; + } + if (Guid.TryParse(sessionId, out var guid)) + { + return guid; + } + + return null; + } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySessionConsts.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySessionConsts.cs index 7182afa28e..96cb8df5c9 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySessionConsts.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySessionConsts.cs @@ -2,7 +2,11 @@ namespace Volo.Abp.Identity; public class IdentitySessionConsts { - public static int MaxDeviceLength { get; set; } = 128; + public static int MaxSessionIdLength { get; set; } = 128; + + public static int MaxDeviceLength { get; set; } = 64; + + public static int MaxDeviceInfoLength { get; set; } = 64; public static int MaxClientIdLength { get; set; } = 64; diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySessionDevices.cs b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySessionDevices.cs new file mode 100644 index 0000000000..989841d034 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentitySessionDevices.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Identity; + +public static class IdentitySessionDevices +{ + public const string Web = "Web"; + + public const string OAuth = "OAuth"; +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionRepository.cs index 2cb0875674..b425b24c33 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentitySessionRepository.cs @@ -1,9 +1,20 @@ using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; namespace Volo.Abp.Identity; public interface IIdentitySessionRepository : IBasicRepository { + Task FindAsync(string sessionId, CancellationToken cancellationToken = default); + Task GetAsync(string sessionId, CancellationToken cancellationToken = default); + + Task> GetListAsync(Guid userId, CancellationToken cancellationToken = default); + + Task DeleteAllAsync(Guid userId, Guid? exceptSessionId = null, CancellationToken cancellationToken = default); + + Task DeleteAllAsync(Guid userId, string device, Guid? exceptSessionId = null, CancellationToken cancellationToken = default); } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySession.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySession.cs index 0dd53c5f74..9e63b8e0cc 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySession.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySession.cs @@ -1,16 +1,21 @@ using System; using System.Collections.Generic; using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; namespace Volo.Abp.Identity; -public class IdentitySession : BasicAggregateRoot +public class IdentitySession : BasicAggregateRoot, IMultiTenant { + public virtual string SessionId { get; protected set; } + /// - /// Web, CLI, STUDIO, ... + /// Web, OAuth ... /// public virtual string Device { get; protected set; } + public virtual string DeviceInfo { get; protected set; } + public virtual Guid? TenantId { get; protected set; } public virtual Guid UserId { get; protected set; } @@ -30,16 +35,20 @@ public class IdentitySession : BasicAggregateRoot public IdentitySession( Guid id, + string sessionId, string device, + string deviceInfo, Guid userId, Guid? tenantId, string clientId, string ipAddresses, DateTime signedIn, - DateTime? lastAccessed) + DateTime? lastAccessed = null) { Id = id; + SessionId = sessionId; Device = device; + DeviceInfo = deviceInfo; UserId = userId; TenantId = tenantId; ClientId = clientId; diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySessionManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySessionManager.cs new file mode 100644 index 0000000000..012ddd87ab --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySessionManager.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Services; +using Volo.Abp.Users; + +namespace Volo.Abp.Identity; + +public class IdentitySessionManager : DomainService +{ + protected IIdentitySessionRepository IdentitySessionRepository { get; } + protected ICurrentUser CurrentUser { get; } + + public IdentitySessionManager(IIdentitySessionRepository identitySessionRepository, ICurrentUser currentUser) + { + IdentitySessionRepository = identitySessionRepository; + CurrentUser = currentUser; + } + + public virtual async Task CreateAsync( + string sessionId, + string device, + string deviceInfo, + Guid userId, + Guid? tenantId, + string clientId, + string ipAddresses) + { + Check.NotNullOrWhiteSpace(sessionId, nameof(sessionId)); + Check.NotNullOrWhiteSpace(device, nameof(device)); + + var session = new IdentitySession( + GuidGenerator.Create(), + sessionId, + device, + deviceInfo, + userId, + tenantId, + clientId, + ipAddresses, + Clock.Now + ); + + return await IdentitySessionRepository.InsertAsync(session); + } + + public virtual async Task UpdateAsync(IdentitySession session) + { + await IdentitySessionRepository.UpdateAsync(session); + } + + public virtual async Task> GetListAsync(Guid userId) + { + return await IdentitySessionRepository.GetListAsync(userId); + } + + public virtual async Task GetAsync(Guid id) + { + return await IdentitySessionRepository.GetAsync(id); + } + + public virtual async Task FindAsync(Guid id) + { + return await IdentitySessionRepository.FindAsync(id); + } + + public virtual async Task GetAsync(string sessionId) + { + return await IdentitySessionRepository.GetAsync(sessionId); + } + + public virtual async Task FindAsync(string sessionId) + { + return await IdentitySessionRepository.FindAsync(sessionId); + } + + public virtual async Task RevokeAsync(Guid id) + { + var session = await IdentitySessionRepository.GetAsync(id); + await IdentitySessionRepository.DeleteAsync(session); + } + + public virtual async Task RevokeAsync(string sessionId) + { + var session = await IdentitySessionRepository.GetAsync(sessionId); + await IdentitySessionRepository.DeleteAsync(session); + } + + public virtual async Task RevokeAllAsync(Guid userId, Guid? exceptSessionId = null) + { + await IdentitySessionRepository.DeleteAllAsync(userId, exceptSessionId); + } + + public virtual async Task RevokeAllAsync(Guid userId, string device, Guid? exceptSessionId = null) + { + await IdentitySessionRepository.DeleteAllAsync(userId, device, exceptSessionId); + } +} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs index e86e57d0ee..5a583531aa 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentitySessionRepository.cs @@ -1,4 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -12,4 +19,34 @@ public class EfCoreIdentitySessionRepository : EfCoreRepository FindAsync(string sessionId, CancellationToken cancellationToken = default) + { + return await (await GetDbSetAsync()).FirstOrDefaultAsync(x => x.SessionId == sessionId, GetCancellationToken(cancellationToken)); + } + + public virtual async Task GetAsync(string sessionId, CancellationToken cancellationToken = default) + { + var session = await FindAsync(sessionId, cancellationToken); + if (session == null) + { + throw new EntityNotFoundException(typeof(IdentitySession)); + } + + return session; + } + + public virtual async Task> GetListAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await (await GetDbSetAsync()).Where(x => x.UserId == userId).ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task DeleteAllAsync(Guid userId, Guid? exceptSessionId = null, CancellationToken cancellationToken = default) + { + await DeleteDirectAsync(x => x.UserId == userId && x.Id != exceptSessionId, cancellationToken); + } + + public virtual async Task DeleteAllAsync(Guid userId, string device, Guid? exceptSessionId = null, CancellationToken cancellationToken = default) + { + await DeleteDirectAsync(x => x.UserId == userId && x.Device == device && x.Id != exceptSessionId, cancellationToken); + } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs index 6258f1b24b..f94f4690cc 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs @@ -285,11 +285,14 @@ public static class IdentityDbContextModelBuilderExtensions b.ConfigureByConvention(); + b.Property(x => x.SessionId).HasMaxLength(IdentitySessionConsts.MaxSessionIdLength).IsRequired(); b.Property(x => x.Device).HasMaxLength(IdentitySessionConsts.MaxDeviceLength).IsRequired(); + b.Property(x => x.DeviceInfo).HasMaxLength(IdentitySessionConsts.MaxDeviceInfoLength); b.Property(x => x.ClientId).HasMaxLength(IdentitySessionConsts.MaxClientIdLength); b.Property(x => x.IpAddresses).HasMaxLength(IdentitySessionConsts.MaxIpAddressesLength); - b.HasIndex(x => new { x.Device }); + b.HasIndex(x => x.SessionId); + b.HasIndex(x => x.Device ); b.HasIndex(x => new { x.TenantId, x.UserId }); b.ApplyObjectExtensionMappings(); diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentitySessionRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentitySessionRepository.cs index 9f873165a0..222d1d432b 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentitySessionRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentitySessionRepository.cs @@ -1,4 +1,10 @@ using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.MongoDB; @@ -11,4 +17,39 @@ public class MongoIdentitySessionRepository : MongoDbRepository FindAsync(string sessionId, CancellationToken cancellationToken = default) + { + return await (await GetMongoQueryableAsync(GetCancellationToken(cancellationToken))) + .As>() + .FirstOrDefaultAsync(x => x.SessionId == sessionId, GetCancellationToken(cancellationToken)); + } + + public virtual async Task GetAsync(string sessionId, CancellationToken cancellationToken = default) + { + var session = await FindAsync(sessionId, cancellationToken); + if (session == null) + { + throw new EntityNotFoundException(typeof(IdentitySession)); + } + + return session; + } + + public virtual async Task> GetListAsync(Guid userId, CancellationToken cancellationToken = default) + { + return await (await GetMongoQueryableAsync(GetCancellationToken(cancellationToken))) + .As>() + .Where(x => x.UserId == userId).ToListAsync(GetCancellationToken(cancellationToken)); + } + + public virtual async Task DeleteAllAsync(Guid userId, Guid? exceptSessionId = null, CancellationToken cancellationToken = default) + { + await DeleteDirectAsync(x => x.UserId == userId && x.Id != exceptSessionId, cancellationToken); + } + + public virtual async Task DeleteAllAsync(Guid userId, string device, Guid? exceptSessionId = null, CancellationToken cancellationToken = default) + { + await DeleteDirectAsync(x => x.UserId == userId && x.Device == device && x.Id != exceptSessionId, cancellationToken); + } }