< Summary

Class:Azure.Security.KeyVault.ChallengeBasedAuthenticationPolicy
Assembly:Azure.Security.KeyVault.Secrets
File(s):C:\Git\azure-sdk-for-net\sdk\keyvault\Azure.Security.KeyVault.Shared\src\ChallengeBasedAuthenticationPolicy.cs
Covered lines:91
Uncovered lines:8
Coverable lines:99
Total lines:286
Line coverage:91.9% (91 of 99)
Covered branches:53
Total branches:62
Branch coverage:85.4% (53 of 62)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.ctor(...)-100%100%
Process(...)-100%100%
ProcessAsync(...)-100%100%
ProcessCoreAsync()-100%100%
AuthenticateRequestAsync()-100%100%
.cctor()-100%100%
.ctor(...)-100%100%
get_Authority()-100%100%
get_Scopes()-100%100%
Equals(...)-66.67%50%
GetHashCode()-0%100%
GetChallenge(...)-100%100%
ClearCache()-100%100%
GetChallengeFromResponse(...)-100%100%
ParseBearerChallengeHeaderValue(...)-77.27%70%
GetRequestAuthority(...)-100%100%

File(s)

C:\Git\azure-sdk-for-net\sdk\keyvault\Azure.Security.KeyVault.Shared\src\ChallengeBasedAuthenticationPolicy.cs

#LineLine coverage
 1// Copyright (c) Microsoft Corporation. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using Azure.Core;
 5using Azure.Core.Pipeline;
 6using System;
 7using System.Collections.Generic;
 8using System.Globalization;
 9using System.Threading.Tasks;
 10
 11namespace Azure.Security.KeyVault
 12{
 13    internal class ChallengeBasedAuthenticationPolicy : HttpPipelinePolicy
 14    {
 15        private const string BearerChallengePrefix = "Bearer ";
 16
 17        private readonly TokenCredential _credential;
 18
 19        private AuthenticationChallenge _challenge = null;
 20        private string _headerValue;
 21        private DateTimeOffset _refreshOn;
 22
 17823        public ChallengeBasedAuthenticationPolicy(TokenCredential credential)
 24        {
 17825            _credential = credential;
 17826        }
 27
 28        public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
 29        {
 50430            ProcessCoreAsync(message, pipeline, false).EnsureCompleted();
 50231        }
 32
 33        public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
 34        {
 57035            return ProcessCoreAsync(message, pipeline, true);
 36        }
 37
 38        private async ValueTask ProcessCoreAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline, bool 
 39        {
 107440            if (message.Request.Uri.Scheme != Uri.UriSchemeHttps)
 41            {
 442                throw new InvalidOperationException("Bearer token authentication is not permitted for non TLS protected 
 43            }
 44
 107045            RequestContent originalContent = message.Request.Content;
 46
 47            // if this policy doesn't have _challenge cached try to get it from the static challenge cache
 107048            AuthenticationChallenge challenge = _challenge ?? AuthenticationChallenge.GetChallenge(message);
 49
 50            // if we still don't have the challenge for the endpoint
 51            // remove the content from the request and send without authentication to get the challenge
 107052            if (challenge == null)
 53            {
 9654                message.Request.Content = null;
 55            }
 56            // otherwise if we already know the challenge authenticate the request
 57            else
 58            {
 97459                await AuthenticateRequestAsync(message, async, challenge).ConfigureAwait(false);
 60            }
 61
 107062            if (async)
 63            {
 56864                await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
 65            }
 66            else
 67            {
 50268                ProcessNext(message, pipeline);
 69            }
 70
 71            // if we get a 401
 107072            if (message.Response.Status == 401)
 73            {
 74                // set the content to the original content in case it was cleared
 9875                message.Request.Content = originalContent;
 76
 77                // update the cached challenge
 9878                challenge = AuthenticationChallenge.GetChallenge(message);
 79
 9880                if (challenge != null)
 81                {
 82                    // update the cached challenge if not yet set or different from the current challenge (e.g. moved te
 9883                    if (_challenge == null || !challenge.Equals(_challenge))
 84                    {
 8685                        _challenge = challenge;
 86                    }
 87
 88                    // authenticate the request and resend
 9889                    await AuthenticateRequestAsync(message, async, challenge).ConfigureAwait(false);
 90
 9891                    if (async)
 92                    {
 5693                        await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
 94                    }
 95                    else
 96                    {
 4297                        ProcessNext(message, pipeline);
 98                    }
 99                }
 100            }
 1070101        }
 102
 103        private async Task AuthenticateRequestAsync(HttpMessage message, bool async, AuthenticationChallenge challenge)
 104        {
 1072105            if (_headerValue is null || DateTimeOffset.UtcNow >= _refreshOn)
 106            {
 134107                AccessToken token = async ?
 134108                        await _credential.GetTokenAsync(new TokenRequestContext(challenge.Scopes, message.Request.Client
 134109                        _credential.GetToken(new TokenRequestContext(challenge.Scopes, message.Request.ClientRequestId),
 110
 134111                _headerValue = BearerChallengePrefix + token.Token;
 134112                _refreshOn = token.ExpiresOn - TimeSpan.FromMinutes(2);
 113            }
 114
 1072115            message.Request.Headers.SetValue(HttpHeader.Names.Authorization, _headerValue);
 1072116        }
 117
 118        internal class AuthenticationChallenge
 119        {
 4120            private static readonly Dictionary<string, AuthenticationChallenge> s_cache = new Dictionary<string, Authent
 4121            private static readonly object s_cacheLock = new object();
 4122            private static readonly string[] s_challengeDelimiters = new string[] { "," };
 123
 98124            private AuthenticationChallenge(string authority, string scope)
 125            {
 98126                Authority = authority;
 98127                Scopes = new string[] { scope };
 98128            }
 129
 24130            public string Authority { get; }
 131
 158132            public string[] Scopes { get; }
 133
 134            public override bool Equals(object obj)
 135            {
 12136                if (ReferenceEquals(this, obj))
 137                {
 0138                    return true;
 139                }
 140
 141                // This assumes that Authority Scopes are always non-null and Scopes has a length of one.
 142                // This is guaranteed by the way the AuthenticationChallenge cache is constructed.
 12143                if (obj is AuthenticationChallenge other)
 144                {
 12145                    return string.Equals(Authority, other.Authority, StringComparison.OrdinalIgnoreCase)
 12146                        && string.Equals(Scopes[0], other.Scopes[0], StringComparison.OrdinalIgnoreCase);
 147                }
 148
 0149                return false;
 150            }
 151
 152            public override int GetHashCode()
 153            {
 154                // Currently the hash code is simply the hash of the authority and first scope as this is what is used t
 155                // This assumes that Authority Scopes are always non-null and Scopes has a length of one.
 156                // This is guaranteed by the way the AuthenticationChallenge cache is constructed.
 0157                return HashCodeBuilder.Combine(Authority, Scopes[0]);
 158            }
 159
 160            public static AuthenticationChallenge GetChallenge(HttpMessage message)
 161            {
 242162                AuthenticationChallenge challenge = null;
 163
 242164                if (message.HasResponse)
 165                {
 98166                    challenge = GetChallengeFromResponse(message.Response);
 167
 168                    // if the challenge is non-null cache it
 98169                    if (challenge != null)
 170                    {
 98171                        string authority = GetRequestAuthority(message.Request);
 98172                        lock (s_cacheLock)
 173                        {
 98174                            s_cache[authority] = challenge;
 98175                        }
 176                    }
 177                }
 178                else
 179                {
 180                    // try to get the challenge from the cache
 144181                    string authority = GetRequestAuthority(message.Request);
 144182                    lock (s_cacheLock)
 183                    {
 144184                        s_cache.TryGetValue(authority, out challenge);
 144185                    }
 186                }
 187
 242188                return challenge;
 189            }
 190
 191            internal static void ClearCache()
 192            {
 193                // try to get the challenge from the cache
 80194                lock (s_cacheLock)
 195                {
 80196                    s_cache.Clear();
 80197                }
 80198            }
 199
 200            private static AuthenticationChallenge GetChallengeFromResponse(Response response)
 201            {
 98202                AuthenticationChallenge challenge = null;
 203
 98204                if (response.Headers.TryGetValue("WWW-Authenticate", out string challengeValue) && challengeValue.Starts
 205                {
 98206                    challenge = ParseBearerChallengeHeaderValue(challengeValue);
 207                }
 208
 98209                return challenge;
 210            }
 211
 212            private static AuthenticationChallenge ParseBearerChallengeHeaderValue(string challengeValue)
 213            {
 98214                string authority = null;
 98215                string scope = null;
 216
 217                // remove the bearer challenge prefix
 98218                var trimmedChallenge = challengeValue.Substring(BearerChallengePrefix.Length);
 219
 220                // Split the trimmed challenge into a set of name=value strings that
 221                // are comma separated. The value fields are expected to be within
 222                // quotation characters that are stripped here.
 98223                string[] pairs = trimmedChallenge.Split(s_challengeDelimiters, StringSplitOptions.RemoveEmptyEntries);
 224
 98225                if (pairs.Length > 0)
 226                {
 227                    // Process the name=value string
 588228                    for (int i = 0; i < pairs.Length; i++)
 229                    {
 196230                        string[] pair = pairs[i].Split('=');
 231
 196232                        if (pair.Length == 2)
 233                        {
 234                            // We have a key and a value, now need to trim and decode
 196235                            string key = pair[0].AsSpan().Trim().Trim('\"').ToString();
 196236                            string value = pair[1].AsSpan().Trim().Trim('\"').ToString();
 237
 196238                            if (!string.IsNullOrEmpty(key))
 239                            {
 240                                // Ordered by current likelihood.
 196241                                if (string.Equals(key, "authorization", StringComparison.OrdinalIgnoreCase))
 242                                {
 98243                                    authority = value;
 244                                }
 98245                                else if (string.Equals(key, "resource", StringComparison.OrdinalIgnoreCase))
 246                                {
 98247                                    scope = value + "/.default";
 248                                }
 0249                                else if (string.Equals(key, "scope", StringComparison.OrdinalIgnoreCase))
 250                                {
 0251                                    scope = value;
 252                                }
 0253                                else if (string.Equals(key, "authorization_uri", StringComparison.OrdinalIgnoreCase))
 254                                {
 0255                                    authority = value;
 256                                }
 257                            }
 258                        }
 259                    }
 260                }
 261
 98262                if (authority != null && scope != null)
 263                {
 98264                    return new AuthenticationChallenge(authority, scope);
 265                }
 266
 0267                return null;
 268            }
 269
 270            private static string GetRequestAuthority(Request request)
 271            {
 242272                Uri uri = request.Uri.ToUri();
 273
 242274                string authority = uri.Authority;
 275
 242276                if (!authority.Contains(":") && uri.Port > 0)
 277                {
 278                    // Append port for complete authority
 242279                    authority = uri.Authority + ":" + uri.Port.ToString(CultureInfo.InvariantCulture);
 280                }
 281
 242282                return authority;
 283            }
 284        }
 285    }
 286}