Skip to content

Commit

Permalink
Split Nginx/Runtime 403 exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
terencefan committed Oct 17, 2024
1 parent 5caea7f commit fff5544
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private Task ProcessNegotiationRequest(IOwinContext owinContext, HostContext con
{
return GenerateClientAccessTokenAsync(provider, context, url, claims);
}
catch (AzureSignalRAccessTokenNotAuthorizedException e)
catch (AzureSignalRAccessKeyNotAvailableException e)
{
Log.NegotiateFailed(_logger, e.Message);
context.Response.StatusCode = 500;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -17,10 +18,14 @@

namespace Microsoft.Azure.SignalR;

internal partial class MicrosoftEntraAccessKey : AccessKey
#nullable enable

internal class MicrosoftEntraAccessKey : AccessKey
{
internal static readonly TimeSpan GetAccessKeyTimeout = TimeSpan.FromSeconds(100);

internal Func<HttpResponseMessage, Task<bool>> HttpResponseHandler;

private const int GetAccessKeyIntervalInMinute = 55;

private const int GetAccessKeyMaxRetryTimes = 3;
Expand All @@ -37,11 +42,11 @@ internal partial class MicrosoftEntraAccessKey : AccessKey

private static readonly TimeSpan GetAccessKeyRetryInterval = TimeSpan.FromSeconds(3);

private readonly TaskCompletionSource<object> _initializedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource<object?> _initializedTcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);

private volatile bool _isAuthorized = false;
private readonly IHttpClientFactory _httpClientFactory;

private Exception _lastException;
private volatile bool _isAuthorized = false;

private DateTime _lastUpdatedTime = DateTime.MinValue;

Expand All @@ -52,7 +57,7 @@ private set
{
if (value)
{
_lastException = null;
LastException = null;
}
_lastUpdatedTime = DateTime.UtcNow;
_isAuthorized = value;
Expand All @@ -62,22 +67,30 @@ private set

public TokenCredential TokenCredential { get; }

internal Exception? LastException { get; private set; }

internal string GetAccessKeyUrl { get; }

internal bool HasExpired => DateTime.UtcNow - _lastUpdatedTime > TimeSpan.FromMinutes(GetAccessKeyIntervalInMinute * 2);

private Task<object> InitializedTask => _initializedTcs.Task;
private Task<object?> InitializedTask => _initializedTcs.Task;

public MicrosoftEntraAccessKey(Uri endpoint, TokenCredential credential, Uri serverEndpoint = null) : base(endpoint)
public MicrosoftEntraAccessKey(Uri endpoint,
TokenCredential credential,
Uri? serverEndpoint = null,
IHttpClientFactory? httpClientFactory = null) : base(endpoint)
{
var authorizeUri = (serverEndpoint ?? endpoint).Append("/api/v1/auth/accessKey");
GetAccessKeyUrl = authorizeUri.AbsoluteUri;
TokenCredential = credential;
HttpResponseHandler = HandleHttpResponseAsync;

_httpClientFactory = httpClientFactory ?? HttpClientFactory.Instance;
}

public virtual async Task<string> GetMicrosoftEntraTokenAsync(CancellationToken ctoken = default)
{
Exception latest = null;
Exception? latest = null;
for (var i = 0; i < GetMicrosoftEntraTokenMaxRetryTimes; i++)
{
try
Expand All @@ -90,7 +103,7 @@ public virtual async Task<string> GetMicrosoftEntraTokenAsync(CancellationToken
latest = e;
}
}
throw latest;
throw latest ?? new InvalidOperationException();
}

public override async Task<string> GenerateAccessTokenAsync(
Expand All @@ -107,7 +120,7 @@ public override async Task<string> GenerateAccessTokenAsync(
await task;
return IsAuthorized
? await base.GenerateAccessTokenAsync(audience, claims, lifetime, algorithm, ctoken)
: throw new AzureSignalRAccessTokenNotAuthorizedException(TokenCredential.GetType().Name, _lastException);
: throw new AzureSignalRAccessKeyNotAvailableException(TokenCredential, LastException);
}
else
{
Expand All @@ -121,6 +134,22 @@ internal void UpdateAccessKey(string kid, string accessKey)
IsAuthorized = true;
}

internal async Task GetAccessKeyAsync(CancellationToken ctoken)
{
var accessToken = await GetMicrosoftEntraTokenAsync(ctoken);

var request = new HttpRequestMessage(HttpMethod.Get, new Uri(GetAccessKeyUrl));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

var httpClient = _httpClientFactory.CreateClient(Constants.HttpClientNames.UserDefault);

var response = await httpClient.SendAsync(request, ctoken);

await HandleHttpResponseAsync(response);

await ThrowExceptionOnResponseFailureAsync(response);
}

internal async Task UpdateAccessKeyAsync(CancellationToken ctoken = default)
{
var delta = DateTime.UtcNow - _lastUpdatedTime;
Expand All @@ -139,18 +168,16 @@ internal async Task UpdateAccessKeyAsync(CancellationToken ctoken = default)
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(source.Token, ctoken);
try
{
var token = await GetMicrosoftEntraTokenAsync(linkedSource.Token);
await GetAccessKeyInternalAsync(token, linkedSource.Token);
return;
await GetAccessKeyAsync(linkedSource.Token);
}
catch (OperationCanceledException e)
{
_lastException = e;
LastException = e;
break;
}
catch (Exception e)
{
_lastException = e;
LastException = e;
try
{
await Task.Delay(GetAccessKeyRetryInterval, ctoken);
Expand All @@ -165,32 +192,58 @@ internal async Task UpdateAccessKeyAsync(CancellationToken ctoken = default)
IsAuthorized = false;
}

private async Task GetAccessKeyInternalAsync(string accessToken, CancellationToken ctoken = default)
private static async Task ThrowExceptionOnResponseFailureAsync(HttpResponseMessage response)
{
var api = new RestApiEndpoint(GetAccessKeyUrl, accessToken);
var client = new RestClient(HttpClientFactory.Instance);
await client.SendAsync(
api,
HttpMethod.Get,
handleExpectedResponseAsync: HandleHttpResponseAsync,
cancellationToken: ctoken);
if (response.IsSuccessStatusCode)
{
return;
}

var detail = await response.Content.ReadAsStringAsync();

#if NET5_0_OR_GREATER
var innerException = new HttpRequestException(
$"Response status code does not indicate success: {(int)response.StatusCode} ({response.ReasonPhrase})",
null,
response.StatusCode);
#else
var innerException = new HttpRequestException(
$"Response status code does not indicate success: {(int)response.StatusCode} ({response.ReasonPhrase})");
#endif

var requestUri = response.RequestMessage?.RequestUri?.ToString();
throw response.StatusCode switch
{
HttpStatusCode.BadRequest => new AzureSignalRInvalidArgumentException(requestUri, innerException, detail),
HttpStatusCode.Unauthorized => new AzureSignalRUnauthorizedException(requestUri, innerException),
HttpStatusCode.Forbidden => new AzureSignalRForbiddenException(requestUri, innerException),
HttpStatusCode.NotFound => new AzureSignalRInaccessibleEndpointException(requestUri, innerException),
_ => new AzureSignalRRuntimeException(requestUri, innerException),
};
}

private async Task<bool> HandleHttpResponseAsync(HttpResponseMessage response)
{
if (response.StatusCode != HttpStatusCode.OK)
string content;
if (response.StatusCode == HttpStatusCode.Forbidden)
{
content = await response.Content.ReadAsStringAsync();
response.ReasonPhrase = content.Contains("nginx") ? Constants.IngressDenied : Constants.RuntimeDenied;
return false;
}
else if (response.StatusCode != HttpStatusCode.OK)
{
return false;
}

var json = await response.Content.ReadAsStringAsync();
var obj = JObject.Parse(json);
content = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(content);

if (!obj.TryGetValue("KeyId", out var keyId) || keyId.Type != JTokenType.String)
if (!json.TryGetValue("KeyId", out var keyId) || keyId.Type != JTokenType.String)
{
throw new AzureSignalRException("Missing required <KeyId> field.");
}
if (!obj.TryGetValue("AccessKey", out var key) || key.Type != JTokenType.String)
if (!json.TryGetValue("AccessKey", out var key) || key.Type != JTokenType.String)
{
throw new AzureSignalRException("Missing required <AccessKey> field.");
}
Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.Azure.SignalR.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ internal static class Constants

public const string AsrsIsDiagnosticClient = "Asrs-Is-Diagnostic-Client";

public const string IngressDenied = "Nginx denied the access, please check your Networking settings.";

public const string RuntimeDenied = "Azure SignalR service denied the access, please check your Access control (IAM) settings.";

public static class Keys
{
public const string AzureSignalRSectionKey = "Azure:SignalR";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Azure.Core;

namespace Microsoft.Azure.SignalR.Common;

internal class AzureSignalRAccessKeyNotAvailableException : AzureSignalRException
{
private const string Template = "The [{0}] is not available to generate access tokens for negotiation.";

public AzureSignalRAccessKeyNotAvailableException(TokenCredential credential, Exception innerException) :
base(BuildExceptionMessage(credential), innerException)
{
}

private static string BuildExceptionMessage(TokenCredential credential)
{
return string.Format(Template, credential.GetType().Name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.Azure.SignalR.Common
/// <summary>
/// The exception throws when AccessKey is not authorized.
/// </summary>
[Obsolete]
public class AzureSignalRAccessTokenNotAuthorizedException : AzureSignalRException
{
private const string Postfix = " appears to lack the permission to generate access tokens, see innerException for more details.";
Expand Down
Loading

0 comments on commit fff5544

Please sign in to comment.