< Summary - CoreWCF Coverage — PR #1766

Information
Class: CoreWCF.IdentityModel.Tokens.DefaultTokenReplayCache
Assembly: CoreWCF.Primitives
File(s): /home/runner/work/CoreWCF/CoreWCF/src/CoreWCF.Primitives/src/CoreWCF/IdentityModel/Tokens/DefaultTokenReplayCache.cs
Line coverage
64%
Covered lines: 27
Uncovered lines: 15
Coverable lines: 42
Total lines: 142
Line coverage: 64.2%
Branch coverage
45%
Covered branches: 11
Total branches: 24
Branch coverage: 45.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%11100%
.ctor()100%11100%
.ctor(...)100%44100%
Contains(...)100%22100%
Remove(...)100%11100%
TryAdd(...)100%44100%
TryFind(...)100%11100%
PurgeIfNeeded()25%4444.44%
Purge()0%10100%

File(s)

/home/runner/work/CoreWCF/CoreWCF/src/CoreWCF.Primitives/src/CoreWCF/IdentityModel/Tokens/DefaultTokenReplayCache.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Collections.Concurrent;
 7using System.Threading;
 8
 9namespace CoreWCF.IdentityModel.Tokens
 10{
 11    /// <summary>
 12    /// A default implementation of the Token replay cache that is backed by
 13    /// a bounded, expiring in-memory cache.
 14    /// </summary>
 15    internal class DefaultTokenReplayCache : TokenReplayCache
 16    {
 217        private static readonly int s_defaultTokenReplayCacheCapacity = 500000;
 218        private static readonly TimeSpan s_defaultTokenReplayCachePurgeInterval = TimeSpan.FromMinutes(1);
 19
 20        private readonly ConcurrentDictionary<string, DateTime> _items;
 21        private readonly int _capacity;
 22        private readonly TimeSpan _purgeInterval;
 23        private long _nextPurgeTicks;
 24
 25        /// <summary>
 26        /// Constructs the default token replay cache.
 27        /// </summary>
 28        public DefaultTokenReplayCache()
 1029            : this(s_defaultTokenReplayCacheCapacity, s_defaultTokenReplayCachePurgeInterval)
 1030        { }
 31
 32        /// <summary>
 33        /// Constructs the default token replay cache with the specified
 34        /// capacity and purge interval.
 35        /// </summary>
 36        /// <param name="capacity">The capacity of the token cache.</param>
 37        /// <param name="purgeInterval">The time interval after which expired entries are removed.</param>
 38        public DefaultTokenReplayCache(int capacity, TimeSpan purgeInterval)
 1939            : base()
 40        {
 1941            if (capacity <= 0)
 42            {
 143                throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.Format(SR.ID0002));
 44            }
 45
 1846            if (purgeInterval <= TimeSpan.Zero)
 47            {
 148                throw new ArgumentOutOfRangeException(nameof(purgeInterval), purgeInterval, SR.Format(SR.ID0016));
 49            }
 50
 1751            _capacity = capacity;
 1752            _purgeInterval = purgeInterval;
 1753            _items = new ConcurrentDictionary<string, DateTime>(StringComparer.Ordinal);
 1754            _nextPurgeTicks = DateTime.UtcNow.Add(purgeInterval).Ticks;
 1755        }
 56
 57        /// <summary>
 58        /// Returns true when the cache contains a non-expired entry for the supplied key.
 59        /// Expired entries are treated as absent and are removed on the next purge.
 60        /// </summary>
 61        public override bool Contains(string key)
 62        {
 363            return _items.TryGetValue(key, out DateTime expiresOn) && DateTime.UtcNow < expiresOn;
 64        }
 65
 66        /// <summary>
 67        /// Removes the entry with the supplied key, if present.
 68        /// </summary>
 169        public override void Remove(string key) => _items.TryRemove(key, out _);
 70
 71        public override bool TryAdd(string securityToken, DateTime expiresOn)
 72        {
 1639673            if (DateTime.Equals(expiresOn, DateTime.MaxValue))
 74            {
 175                throw DiagnosticUtility.ExceptionUtility.ThrowHelperInvalidOperation(SR.Format(SR.ID1072));
 76            }
 77
 1639578            PurgeIfNeeded();
 79
 80            // Capacity is enforced approximately: Count is sampled before TryAdd, so
 81            // the live count can briefly exceed _capacity by the number of in-flight
 82            // concurrent inserts. This is acceptable because the bound only exists to
 83            // prevent unbounded growth, and the slack is bounded by request concurrency.
 1639584            if (_items.Count >= _capacity)
 85            {
 186                throw new QuotaExceededException(SR.Format(SR.ID0021, _capacity));
 87            }
 88
 89            // ConcurrentDictionary.TryAdd is an atomic add-if-absent. It is exactly the
 90            // primitive the replay-cache contract requires: when this returns false the
 91            // caller (Microsoft.IdentityModel.Tokens.Saml) raises
 92            // SecurityTokenReplayDetectedException. Two concurrent inserts of the same
 93            // key cannot both observe "absent", so duplicate tokens are reliably rejected
 94            // without a TOCTOU window.
 1639495            return _items.TryAdd(securityToken, expiresOn);
 96        }
 97
 198        public override bool TryFind(string securityToken) => Contains(securityToken);
 99
 100        private void PurgeIfNeeded()
 101        {
 16395102            long nowTicks = DateTime.UtcNow.Ticks;
 16395103            long nextTicks = Interlocked.Read(ref _nextPurgeTicks);
 16395104            if (nowTicks < nextTicks)
 105            {
 16395106                return;
 107            }
 108
 0109            long newNext = DateTime.UtcNow.Add(_purgeInterval).Ticks;
 0110            if (Interlocked.CompareExchange(ref _nextPurgeTicks, newNext, nextTicks) != nextTicks)
 111            {
 112                // Another thread already moved the purge window forward and owns this round.
 0113                return;
 114            }
 115
 0116            Purge();
 0117        }
 118
 119        private void Purge()
 120        {
 0121            DateTime now = DateTime.UtcNow;
 0122            List<string> expiredKeys = null;
 0123            foreach (KeyValuePair<string, DateTime> pair in _items)
 124            {
 0125                if (pair.Value <= now)
 126                {
 0127                    (expiredKeys ?? (expiredKeys = new List<string>())).Add(pair.Key);
 128                }
 129            }
 130
 0131            if (expiredKeys == null)
 132            {
 0133                return;
 134            }
 135
 0136            foreach (string key in expiredKeys)
 137            {
 0138                _items.TryRemove(key, out _);
 139            }
 0140        }
 141    }
 142}