< Summary

Class:Azure.Security.KeyVault.ChallengeBasedAuthenticationPolicy
Assembly:Azure.Security.KeyVault.Keys
File(s):C:\Git\azure-sdk-for-net\sdk\keyvault\Azure.Security.KeyVault.Shared\src\ChallengeBasedAuthenticationPolicy.cs
Covered lines:86
Uncovered lines:13
Coverable lines:99
Total lines:286
Line coverage:86.8% (86 of 99)
Covered branches:49
Total branches:62
Branch coverage:79% (49 of 62)

Metrics

MethodCyclomatic complexity Line coverage Branch coverage
.ctor(...)-100%100%
Process(...)-100%100%
ProcessAsync(...)-100%100%
ProcessCoreAsync()-100%94.44%
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
 115423        public ChallengeBasedAuthenticationPolicy(TokenCredential credential)
 24        {
 115425            _credential = credential;
 115426        }
 27
 28        public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
 29        {
 64830            ProcessCoreAsync(message, pipeline, false).EnsureCompleted();
 64631        }
 32
 33        public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
 34        {
 83835            return ProcessCoreAsync(message, pipeline, true);
 36        }
 37
 38        private async ValueTask ProcessCoreAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline, bool 
 39        {
 148640            if (message.Request.Uri.Scheme != Uri.UriSchemeHttps)
 41            {
 442                throw new InvalidOperationException("Bearer token authentication is not permitted for non TLS protected 
 43            }
 44
 148245            RequestContent originalContent = message.Request.Content;
 46
 47            // if this policy doesn't have _challenge cached try to get it from the static challenge cache
 148248            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
 148252            if (challenge == null)
 53            {
 50654                message.Request.Content = null;
 55            }
 56            // otherwise if we already know the challenge authenticate the request
 57            else
 58            {
 97659                await AuthenticateRequestAsync(message, async, challenge).ConfigureAwait(false);
 60            }
 61
 148262            if (async)
 63            {
 83664                await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
 65            }
 66            else
 67            {
 64668                ProcessNext(message, pipeline);
 69            }
 70
 71            // if we get a 401
 148272            if (message.Response.Status == 401)
 73            {
 74                // set the content to the original content in case it was cleared
 36675                message.Request.Content = originalContent;
 76
 77                // update the cached challenge
 36678                challenge = AuthenticationChallenge.GetChallenge(message);
 79
 36680                if (challenge != null)
 81                {
 82                    // update the cached challenge if not yet set or different from the current challenge (e.g. moved te
 36683                    if (_challenge == null || !challenge.Equals(_challenge))
 84                    {
 36685                        _challenge = challenge;
 86                    }
 87
 88                    // authenticate the request and resend
 36689                    await AuthenticateRequestAsync(message, async, challenge).ConfigureAwait(false);
 90
 36691                    if (async)
 92                    {
 18293                        await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
 94                    }
 95                    else
 96                    {
 18497                        ProcessNext(message, pipeline);
 98                    }
 99                }
 100            }
 1482101        }
 102
 103        private async Task AuthenticateRequestAsync(HttpMessage message, bool async, AuthenticationChallenge challenge)
 104        {
 1342105            if (_headerValue is null || DateTimeOffset.UtcNow >= _refreshOn)
 106            {
 572107                AccessToken token = async ?
 572108                        await _credential.GetTokenAsync(new TokenRequestContext(challenge.Scopes, message.Request.Client
 572109                        _credential.GetToken(new TokenRequestContext(challenge.Scopes, message.Request.ClientRequestId),
 110
 572111                _headerValue = BearerChallengePrefix + token.Token;
 572112                _refreshOn = token.ExpiresOn - TimeSpan.FromMinutes(2);
 113            }
 114
 1342115            message.Request.Headers.SetValue(HttpHeader.Names.Authorization, _headerValue);
 1342116        }
 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
 366124            private AuthenticationChallenge(string authority, string scope)
 125            {
 366126                Authority = authority;
 366127                Scopes = new string[] { scope };
 366128            }
 129
 0130            public string Authority { get; }
 131
 572132            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            {
 1384162                AuthenticationChallenge challenge = null;
 163
 1384164                if (message.HasResponse)
 165                {
 366166                    challenge = GetChallengeFromResponse(message.Response);
 167
 168                    // if the challenge is non-null cache it
 366169                    if (challenge != null)
 170                    {
 366171                        string authority = GetRequestAuthority(message.Request);
 366172                        lock (s_cacheLock)
 173                        {
 366174                            s_cache[authority] = challenge;
 366175                        }
 176                    }
 177                }
 178                else
 179                {
 180                    // try to get the challenge from the cache
 1018181                    string authority = GetRequestAuthority(message.Request);
 1018182                    lock (s_cacheLock)
 183                    {
 1018184                        s_cache.TryGetValue(authority, out challenge);
 1018185                    }
 186                }
 187
 1384188                return challenge;
 189            }
 190
 191            internal static void ClearCache()
 192            {
 193                // try to get the challenge from the cache
 388194                lock (s_cacheLock)
 195                {
 388196                    s_cache.Clear();
 388197                }
 388198            }
 199
 200            private static AuthenticationChallenge GetChallengeFromResponse(Response response)
 201            {
 366202                AuthenticationChallenge challenge = null;
 203
 366204                if (response.Headers.TryGetValue("WWW-Authenticate", out string challengeValue) && challengeValue.Starts
 205                {
 366206                    challenge = ParseBearerChallengeHeaderValue(challengeValue);
 207                }
 208
 366209                return challenge;
 210            }
 211
 212            private static AuthenticationChallenge ParseBearerChallengeHeaderValue(string challengeValue)
 213            {
 366214                string authority = null;
 366215                string scope = null;
 216
 217                // remove the bearer challenge prefix
 366218                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.
 366223                string[] pairs = trimmedChallenge.Split(s_challengeDelimiters, StringSplitOptions.RemoveEmptyEntries);
 224
 366225                if (pairs.Length > 0)
 226                {
 227                    // Process the name=value string
 2196228                    for (int i = 0; i < pairs.Length; i++)
 229                    {
 732230                        string[] pair = pairs[i].Split('=');
 231
 732232                        if (pair.Length == 2)
 233                        {
 234                            // We have a key and a value, now need to trim and decode
 732235                            string key = pair[0].AsSpan().Trim().Trim('\"').ToString();
 732236                            string value = pair[1].AsSpan().Trim().Trim('\"').ToString();
 237
 732238                            if (!string.IsNullOrEmpty(key))
 239                            {
 240                                // Ordered by current likelihood.
 732241                                if (string.Equals(key, "authorization", StringComparison.OrdinalIgnoreCase))
 242                                {
 366243                                    authority = value;
 244                                }
 366245                                else if (string.Equals(key, "resource", StringComparison.OrdinalIgnoreCase))
 246                                {
 366247                                    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
 366262                if (authority != null && scope != null)
 263                {
 366264                    return new AuthenticationChallenge(authority, scope);
 265                }
 266
 0267                return null;
 268            }
 269
 270            private static string GetRequestAuthority(Request request)
 271            {
 1384272                Uri uri = request.Uri.ToUri();
 273
 1384274                string authority = uri.Authority;
 275
 1384276                if (!authority.Contains(":") && uri.Port > 0)
 277                {
 278                    // Append port for complete authority
 1384279                    authority = uri.Authority + ":" + uri.Port.ToString(CultureInfo.InvariantCulture);
 280                }
 281
 1384282                return authority;
 283            }
 284        }
 285    }
 286}