< Summary

Information
Class: Elsa.ExternalAuthentication.Stores.InMemory.InMemoryExternalAuthenticationSessionStore
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Stores/InMemory/InMemoryExternalAuthenticationSessionStore.cs
Line coverage
87%
Covered lines: 90
Uncovered lines: 13
Coverable lines: 103
Total lines: 168
Line coverage: 87.3%
Branch coverage
77%
Covered branches: 37
Total branches: 48
Branch coverage: 77%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
FindAsync(...)66.66%1212100%
FindByIdAsync(...)100%22100%
FindByRefreshTokenHashAsync(...)75%44100%
SaveAsync(...)100%11100%
TryRotateRefreshTokenAsync(...)92.85%141495.45%
RevokeAsync(...)100%44100%
RevokeActiveForConnectionAsync(...)0%2040%
EnsureRefreshTokenHashAvailable(...)100%66100%
Clone(...)50%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Stores/InMemory/InMemoryExternalAuthenticationSessionStore.cs

#LineLine coverage
 1using Elsa.Common;
 2using Elsa.ExternalAuthentication.Contracts;
 3using Elsa.ExternalAuthentication.Models;
 4using Elsa.ExternalAuthentication.Services;
 5
 6namespace Elsa.ExternalAuthentication.Stores.InMemory;
 7
 518public sealed class InMemoryExternalAuthenticationSessionStore(ISystemClock clock) : IExternalAuthenticationSessionStore
 9{
 5110    private readonly object _syncRoot = new();
 5111    private readonly Dictionary<string, ExternalAuthenticationSession> _sessions = new(StringComparer.Ordinal);
 12
 13    public ValueTask<IReadOnlyCollection<ExternalAuthenticationSession>> FindAsync(ExternalAuthenticationSessionFilter f
 14    {
 315        ArgumentNullException.ThrowIfNull(filter);
 316        cancellationToken.ThrowIfCancellationRequested();
 17
 318        lock (_syncRoot)
 19        {
 320            var sessions = _sessions.Values
 521                .Where(x => string.Equals(x.TenantId, filter.TenantId, StringComparison.Ordinal))
 322                .Where(x => filter.UserId is null || string.Equals(x.UserId, filter.UserId, StringComparison.Ordinal))
 323                .Where(x => filter.ConnectionKey is null || string.Equals(x.ConnectionKey, ConnectionRevisionCalculator.
 324                .Where(x => filter.Status is null ||
 325                    filter.Status.Equals("active", StringComparison.OrdinalIgnoreCase) && x.RevokedAt is null ||
 326                    filter.Status.Equals("revoked", StringComparison.OrdinalIgnoreCase) && x.RevokedAt is not null)
 327                .Select(Clone)
 328                .ToArray();
 329            return ValueTask.FromResult<IReadOnlyCollection<ExternalAuthenticationSession>>(sessions);
 30        }
 331    }
 32
 33    public ValueTask<ExternalAuthenticationSession?> FindByIdAsync(string sessionId, CancellationToken cancellationToken
 34    {
 1635        cancellationToken.ThrowIfCancellationRequested();
 36
 1637        lock (_syncRoot)
 1638            return ValueTask.FromResult(_sessions.TryGetValue(sessionId, out var session) ? Clone(session) : null);
 1639    }
 40
 41    public ValueTask<ExternalAuthenticationSession?> FindByRefreshTokenHashAsync(string refreshTokenHash, CancellationTo
 42    {
 643        cancellationToken.ThrowIfCancellationRequested();
 44
 645        if (string.IsNullOrWhiteSpace(refreshTokenHash))
 146            return ValueTask.FromResult<ExternalAuthenticationSession?>(null);
 47
 548        lock (_syncRoot)
 1349            return ValueTask.FromResult(_sessions.Values.SingleOrDefault(x => string.Equals(x.CurrentRefreshTokenHash, r
 550    }
 51
 52    public ValueTask SaveAsync(ExternalAuthenticationSession session, CancellationToken cancellationToken = default)
 53    {
 2754        cancellationToken.ThrowIfCancellationRequested();
 55
 2756        lock (_syncRoot)
 57        {
 2758            EnsureRefreshTokenHashAvailable(session.Id, session.CurrentRefreshTokenHash);
 2459            _sessions[session.Id] = Clone(session);
 2460        }
 61
 2462        return ValueTask.CompletedTask;
 63    }
 64
 65    public ValueTask<ExternalAuthenticationSessionRotationResult> TryRotateRefreshTokenAsync(string sessionId, string re
 66    {
 2667        cancellationToken.ThrowIfCancellationRequested();
 68
 2669        lock (_syncRoot)
 70        {
 2671            if (!_sessions.TryGetValue(sessionId, out var session))
 072                return ValueTask.FromResult<ExternalAuthenticationSessionRotationResult>(new ExternalAuthenticationSessi
 73
 2674            if (session.RevokedAt != null)
 1575                return ValueTask.FromResult<ExternalAuthenticationSessionRotationResult>(new ExternalAuthenticationSessi
 76
 1177            if (session.ExpiresAt <= clock.UtcNow || session.RefreshExpiresAt <= clock.UtcNow)
 78            {
 179                session.RevokedAt = clock.UtcNow;
 180                session.RevocationReason = "expired";
 181                session.ProtectedUpstreamLogoutHint = null;
 182                return ValueTask.FromResult<ExternalAuthenticationSessionRotationResult>(new ExternalAuthenticationSessi
 83            }
 84
 1085            if (session.CurrentRefreshTokenHash is null || !string.Equals(session.CurrentRefreshTokenHash, refreshTokenH
 86            {
 387                session.RevokedAt = clock.UtcNow;
 388                session.RevocationReason = "refresh_token_reuse";
 389                session.ProtectedUpstreamLogoutHint = null;
 390                return ValueTask.FromResult<ExternalAuthenticationSessionRotationResult>(new ExternalAuthenticationSessi
 91            }
 92
 793            EnsureRefreshTokenHashAvailable(session.Id, nextRefreshTokenHash);
 594            session.CurrentRefreshTokenHash = nextRefreshTokenHash;
 595            session.RefreshGeneration++;
 596            session.LastRefreshedAt = refreshedAt;
 597            return ValueTask.FromResult<ExternalAuthenticationSessionRotationResult>(new ExternalAuthenticationSessionRo
 98        }
 2499    }
 100
 101    public ValueTask<bool> RevokeAsync(string sessionId, string reason, DateTimeOffset revokedAt, CancellationToken canc
 102    {
 4103        cancellationToken.ThrowIfCancellationRequested();
 104
 4105        lock (_syncRoot)
 106        {
 4107            if (!_sessions.TryGetValue(sessionId, out var session) || session.RevokedAt != null)
 1108                return ValueTask.FromResult(false);
 109
 3110            session.RevokedAt = revokedAt;
 3111            session.RevocationReason = reason;
 3112            session.ProtectedUpstreamLogoutHint = null;
 3113            return ValueTask.FromResult(true);
 114        }
 4115    }
 116
 117    public ValueTask<int> RevokeActiveForConnectionAsync(string connectionKey, string reason, DateTimeOffset revokedAt, 
 118    {
 0119        ArgumentException.ThrowIfNullOrWhiteSpace(connectionKey);
 0120        cancellationToken.ThrowIfCancellationRequested();
 0121        var normalizedConnectionKey = ConnectionRevisionCalculator.NormalizeKey(connectionKey);
 122
 0123        lock (_syncRoot)
 124        {
 0125            var count = 0;
 0126            foreach (var session in _sessions.Values.Where(x => x.RevokedAt is null && string.Equals(x.ConnectionKey, no
 127            {
 0128                session.RevokedAt = revokedAt;
 0129                session.RevocationReason = reason;
 0130                session.ProtectedUpstreamLogoutHint = null;
 0131                count++;
 132            }
 133
 0134            return ValueTask.FromResult(count);
 135        }
 0136    }
 137
 138    private void EnsureRefreshTokenHashAvailable(string sessionId, string? refreshTokenHash)
 139    {
 34140        if (refreshTokenHash is not null && _sessions.Values.Any(x =>
 49141                !string.Equals(x.Id, sessionId, StringComparison.Ordinal) &&
 49142                string.Equals(x.CurrentRefreshTokenHash, refreshTokenHash, StringComparison.Ordinal)))
 5143            throw new InvalidOperationException("An external authentication session already exists for the supplied refr
 29144    }
 145
 50146    private static ExternalAuthenticationSession Clone(ExternalAuthenticationSession session) => new()
 50147    {
 50148        Id = session.Id,
 50149        AuthenticationClientId = session.AuthenticationClientId,
 50150        TenantId = session.TenantId,
 50151        UserId = session.UserId,
 50152        ConnectionKey = session.ConnectionKey,
 50153        ConnectionMaterialRevision = session.ConnectionMaterialRevision,
 50154        SecretGenerationFingerprint = session.SecretGenerationFingerprint,
 50155        Issuer = session.Issuer,
 50156        SubjectHash = session.SubjectHash,
 50157        ExternalGrants = session.ExternalGrants.ToArray(),
 50158        StartedAt = session.StartedAt,
 50159        LastRefreshedAt = session.LastRefreshedAt,
 50160        ExpiresAt = session.ExpiresAt,
 50161        RefreshExpiresAt = session.RefreshExpiresAt,
 50162        CurrentRefreshTokenHash = session.CurrentRefreshTokenHash,
 50163        RefreshGeneration = session.RefreshGeneration,
 50164        RevokedAt = session.RevokedAt,
 50165        RevocationReason = session.RevocationReason
 50166        ,ProtectedUpstreamLogoutHint = session.ProtectedUpstreamLogoutHint?.ToArray()
 50167    };
 168}