< Summary

Class:Azure.Security.KeyVault.ChallengeBasedAuthenticationPolicy
Assembly:Azure.Security.KeyVault.Administration
File(s):C:\Git\azure-sdk-for-net\sdk\keyvault\Azure.Security.KeyVault.Shared\src\ChallengeBasedAuthenticationPolicy.cs
Covered lines:85
Uncovered lines:14
Coverable lines:99
Total lines:286
Line coverage:85.8% (85 of 99)
Covered branches:48
Total branches:62
Branch coverage:77.4% (48 of 62)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.ctor(...)-100%100%
Process(...)-100%100%
ProcessAsync(...)-100%100%
ProcessCoreAsync()-95.24%88.89%
AuthenticateRequestAsync()-100%100%
.cctor()-100%100%
.ctor(...)-100%100%
get_Authority()-0%100%
get_Scopes()-100%100%
Equals(...)-0%0%
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
 5023        public ChallengeBasedAuthenticationPolicy(TokenCredential credential)
 24        {
 5025            _credential = credential;
 5026        }
 27
 28        public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
 29        {
 5430            ProcessCoreAsync(message, pipeline, false).EnsureCompleted();
 5431        }
 32
 33        public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
 34        {
 6435            return ProcessCoreAsync(message, pipeline, true);
 36        }
 37
 38        private async ValueTask ProcessCoreAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline, bool 
 39        {
 11840            if (message.Request.Uri.Scheme != Uri.UriSchemeHttps)
 41            {
 042                throw new InvalidOperationException("Bearer token authentication is not permitted for non TLS protected 
 43            }
 44
 11845            RequestContent originalContent = message.Request.Content;
 46
 47            // if this policy doesn't have _challenge cached try to get it from the static challenge cache
 11848            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
 11852            if (challenge == null)
 53            {
 4054                message.Request.Content = null;
 55            }
 56            // otherwise if we already know the challenge authenticate the request
 57            else
 58            {
 7859                await AuthenticateRequestAsync(message, async, challenge).ConfigureAwait(false);
 60            }
 61
 11862            if (async)
 63            {
 6464                await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
 65            }
 66            else
 67            {
 5468                ProcessNext(message, pipeline);
 69            }
 70
 71            // if we get a 401
 11872            if (message.Response.Status == 401)
 73            {
 74                // set the content to the original content in case it was cleared
 4075                message.Request.Content = originalContent;
 76
 77                // update the cached challenge
 4078                challenge = AuthenticationChallenge.GetChallenge(message);
 79
 4080                if (challenge != null)
 81                {
 82                    // update the cached challenge if not yet set or different from the current challenge (e.g. moved te
 4083                    if (_challenge == null || !challenge.Equals(_challenge))
 84                    {
 4085                        _challenge = challenge;
 86                    }
 87
 88                    // authenticate the request and resend
 4089                    await AuthenticateRequestAsync(message, async, challenge).ConfigureAwait(false);
 90
 4091                    if (async)
 92                    {
 1893                        await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
 94                    }
 95                    else
 96                    {
 2297                        ProcessNext(message, pipeline);
 98                    }
 99                }
 100            }
 118101        }
 102
 103        private async Task AuthenticateRequestAsync(HttpMessage message, bool async, AuthenticationChallenge challenge)
 104        {
 118105            if (_headerValue is null || DateTimeOffset.UtcNow >= _refreshOn)
 106            {
 40107                AccessToken token = async ?
 40108                        await _credential.GetTokenAsync(new TokenRequestContext(challenge.Scopes, message.Request.Client
 40109                        _credential.GetToken(new TokenRequestContext(challenge.Scopes, message.Request.ClientRequestId),
 110
 40111                _headerValue = BearerChallengePrefix + token.Token;
 40112                _refreshOn = token.ExpiresOn - TimeSpan.FromMinutes(2);
 113            }
 114
 118115            message.Request.Headers.SetValue(HttpHeader.Names.Authorization, _headerValue);
 118116        }
 117
 118        internal class AuthenticationChallenge
 119        {
 2120            private static readonly Dictionary<string, AuthenticationChallenge> s_cache = new Dictionary<string, Authent
 2121            private static readonly object s_cacheLock = new object();
 2122            private static readonly string[] s_challengeDelimiters = new string[] { "," };
 123
 40124            private AuthenticationChallenge(string authority, string scope)
 125            {
 40126                Authority = authority;
 40127                Scopes = new string[] { scope };
 40128            }
 129
 0130            public string Authority { get; }
 131
 40132            public string[] Scopes { get; }
 133
 134            public override bool Equals(object obj)
 135            {
 0136                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.
 0143                if (obj is AuthenticationChallenge other)
 144                {
 0145                    return string.Equals(Authority, other.Authority, StringComparison.OrdinalIgnoreCase)
 0146                        && 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            {
 80162                AuthenticationChallenge challenge = null;
 163
 80164                if (message.HasResponse)
 165                {
 40166                    challenge = GetChallengeFromResponse(message.Response);
 167
 168                    // if the challenge is non-null cache it
 40169                    if (challenge != null)
 170                    {
 40171                        string authority = GetRequestAuthority(message.Request);
 40172                        lock (s_cacheLock)
 173                        {
 40174                            s_cache[authority] = challenge;
 40175                        }
 176                    }
 177                }
 178                else
 179                {
 180                    // try to get the challenge from the cache
 40181                    string authority = GetRequestAuthority(message.Request);
 40182                    lock (s_cacheLock)
 183                    {
 40184                        s_cache.TryGetValue(authority, out challenge);
 40185                    }
 186                }
 187
 80188                return challenge;
 189            }
 190
 191            internal static void ClearCache()
 192            {
 193                // try to get the challenge from the cache
 42194                lock (s_cacheLock)
 195                {
 42196                    s_cache.Clear();
 42197                }
 42198            }
 199
 200            private static AuthenticationChallenge GetChallengeFromResponse(Response response)
 201            {
 40202                AuthenticationChallenge challenge = null;
 203
 40204                if (response.Headers.TryGetValue("WWW-Authenticate", out string challengeValue) && challengeValue.Starts
 205                {
 40206                    challenge = ParseBearerChallengeHeaderValue(challengeValue);
 207                }
 208
 40209                return challenge;
 210            }
 211
 212            private static AuthenticationChallenge ParseBearerChallengeHeaderValue(string challengeValue)
 213            {
 40214                string authority = null;
 40215                string scope = null;
 216
 217                // remove the bearer challenge prefix
 40218                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.
 40223                string[] pairs = trimmedChallenge.Split(s_challengeDelimiters, StringSplitOptions.RemoveEmptyEntries);
 224
 40225                if (pairs.Length > 0)
 226                {
 227                    // Process the name=value string
 240228                    for (int i = 0; i < pairs.Length; i++)
 229                    {
 80230                        string[] pair = pairs[i].Split('=');
 231
 80232                        if (pair.Length == 2)
 233                        {
 234                            // We have a key and a value, now need to trim and decode
 80235                            string key = pair[0].AsSpan().Trim().Trim('\"').ToString();
 80236                            string value = pair[1].AsSpan().Trim().Trim('\"').ToString();
 237
 80238                            if (!string.IsNullOrEmpty(key))
 239                            {
 240                                // Ordered by current likelihood.
 80241                                if (string.Equals(key, "authorization", StringComparison.OrdinalIgnoreCase))
 242                                {
 40243                                    authority = value;
 244                                }
 40245                                else if (string.Equals(key, "resource", StringComparison.OrdinalIgnoreCase))
 246                                {
 40247                                    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
 40262                if (authority != null && scope != null)
 263                {
 40264                    return new AuthenticationChallenge(authority, scope);
 265                }
 266
 0267                return null;
 268            }
 269
 270            private static string GetRequestAuthority(Request request)
 271            {
 80272                Uri uri = request.Uri.ToUri();
 273
 80274                string authority = uri.Authority;
 275
 80276                if (!authority.Contains(":") && uri.Port > 0)
 277                {
 278                    // Append port for complete authority
 80279                    authority = uri.Authority + ":" + uri.Port.ToString(CultureInfo.InvariantCulture);
 280                }
 281
 80282                return authority;
 283            }
 284        }
 285    }
 286}