Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74d40e40a9 | |||
| c81692ee5a | |||
| 7f50987201 |
@@ -54,7 +54,7 @@ jobs:
|
||||
run: dotnet test ${{ env.Solution }} --filter "(Category=Integration)&(Server!=Public)" --configuration Release --no-build --no-restore --verbosity=normal /p:AltCover=true /p:AltCoverAttributeFilter=ExcludeFromCodeCoverage
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: dotnet pack ${{ env.Solution }} --configuration Release --no-build
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -154,10 +154,10 @@ public sealed class Client : ISpeckleGraphQLClient, IClient
|
||||
activity?.SetStatus(SdkActivityStatusCode.Ok);
|
||||
return ret;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.SetStatus(SdkActivityStatusCode.Error);
|
||||
// Don't record exception as it's rethrown.
|
||||
activity?.RecordException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,59 +142,26 @@ public sealed class AccountManager(
|
||||
|
||||
/// <summary>
|
||||
/// Refetches the <paramref name="account"/> information, including <see cref="ServerInfo"/> and <see cref="UserInfo"/>
|
||||
/// If the <see cref="Account.token"/> looks to be expired, this function will also attempt to use the <see cref="Account.refreshToken"/> to refresh it.
|
||||
///
|
||||
/// Will only mutate <paramref name="account"/> in memory only, and only if successful.
|
||||
/// </summary>
|
||||
/// <seealso cref="UpdateAccount"/>
|
||||
/// <param name="account"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="AggregateException">Thrown if</exception>
|
||||
/// <exception cref="GraphQLHttpRequestException"></exception>
|
||||
public async Task UpdateAccountInMemory(Account account, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Uri url = account.serverInfo.migration?.movedTo ?? new(account.serverInfo.url);
|
||||
ActiveUserServerInfoResponse userServerInfo;
|
||||
|
||||
try
|
||||
ActiveUserServerInfoResponse userServerInfo = await accountFactory
|
||||
.GetUserServerInfo(url, account.token, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (userServerInfo.activeUser == null)
|
||||
{
|
||||
userServerInfo = await accountFactory
|
||||
.GetUserServerInfo(url, account.token, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
catch (GraphQLHttpRequestException ex)
|
||||
{
|
||||
// Failed to fetch info, perhaps the token is expired?
|
||||
// Attempt to refresh it
|
||||
TokenExchangeResponse refreshTokenResponse;
|
||||
try
|
||||
{
|
||||
refreshTokenResponse = await authFlow
|
||||
.GetRefreshedToken(
|
||||
account.refreshToken.NotNull("No refresh token provided"),
|
||||
url,
|
||||
AuthApp.ConnectorsV3,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
userServerInfo = await accountFactory
|
||||
.GetUserServerInfo(url, refreshTokenResponse.token, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
throw new AggregateException("Failed to update account information", ex, ex2);
|
||||
}
|
||||
|
||||
account.token = refreshTokenResponse.token;
|
||||
account.refreshToken = refreshTokenResponse.refreshToken;
|
||||
logger.LogInformation(ex, "Account token has been refreshed");
|
||||
}
|
||||
account.userInfo = userServerInfo.activeUser.NotNull();
|
||||
account.userInfo = userServerInfo.activeUser;
|
||||
account.serverInfo = userServerInfo.serverInfo;
|
||||
//This is a bit gross, since id is not marked nullable
|
||||
//but this will force re-generate the id (e.g. if the user's email, or servers url has changed)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
@@ -42,34 +43,45 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
|
||||
|
||||
Uri tokenEndpoint = new(serverUrl, "/oauth/token");
|
||||
string codeVerifier = GenerateCodeVerifier();
|
||||
string challenge;
|
||||
string codeChallengeMethod;
|
||||
var req = await client.GetAsync(tokenEndpoint, cancellationToken).ConfigureAwait(false);
|
||||
Uri authnVerify;
|
||||
using var req = await client.GetAsync(tokenEndpoint, cancellationToken).ConfigureAwait(false);
|
||||
bool useLegacyEndpoint = req.StatusCode != HttpStatusCode.OK;
|
||||
|
||||
if (useLegacyEndpoint)
|
||||
{
|
||||
challenge = codeVerifier;
|
||||
string challenge = codeVerifier; // Old endpoint only supports PKCE "plain" method
|
||||
authnVerify = new($"/authn/verify/{authApp.AppId}/{challenge}", UriKind.Relative);
|
||||
tokenEndpoint = new(serverUrl, "/auth/token");
|
||||
codeChallengeMethod = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
challenge = GenerateCodeChallenge(codeVerifier);
|
||||
codeChallengeMethod = "?code_challenge_method=S256";
|
||||
string challenge = GenerateCodeChallenge(codeVerifier);
|
||||
authnVerify = new($"/authn/verify/{authApp.AppId}/{challenge}?code_challenge_method=S256", UriKind.Relative);
|
||||
}
|
||||
|
||||
Uri endpoint = new(serverUrl, $"/authn/verify/{authApp.AppId}/{challenge}{codeChallengeMethod}");
|
||||
Uri endpoint = new(serverUrl, authnVerify);
|
||||
_ = Process.Start(new ProcessStartInfo(endpoint.ToString()) { UseShellExecute = true });
|
||||
string accessCode = await RunListenerWithTimeout(authApp.CallbackUrl, timeout, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
object body = useLegacyEndpoint
|
||||
? new
|
||||
{
|
||||
appId = authApp.AppId,
|
||||
appSecret = authApp.AppSecret,
|
||||
accessCode = accessCode,
|
||||
challenge = codeVerifier,
|
||||
}
|
||||
: new
|
||||
{
|
||||
appId = authApp.AppId,
|
||||
accessCode = accessCode,
|
||||
codeVerifier = codeVerifier,
|
||||
};
|
||||
|
||||
return await ExchangeAccessCodeForToken(
|
||||
client,
|
||||
accessCode,
|
||||
authApp,
|
||||
useLegacyEndpoint ? challenge : null,
|
||||
!useLegacyEndpoint ? codeVerifier : null,
|
||||
JsonConvert.SerializeObject(body, _serializerSettings),
|
||||
tokenEndpoint,
|
||||
cancellationToken
|
||||
)
|
||||
@@ -141,7 +153,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
|
||||
refreshToken = refreshToken,
|
||||
};
|
||||
|
||||
using var content = new StringContent(JsonConvert.SerializeObject(body));
|
||||
using var content = new StringContent(JsonConvert.SerializeObject(body, _serializerSettings));
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
var response = await client
|
||||
.PostAsync(new Uri(serverUrl, "/auth/token"), content, cancellationToken)
|
||||
@@ -248,24 +260,12 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
|
||||
|
||||
private async Task<TokenExchangeResponse> ExchangeAccessCodeForToken(
|
||||
HttpClient client,
|
||||
string accessCode,
|
||||
AuthApp authApp,
|
||||
string? challenge,
|
||||
string? codeVerifier,
|
||||
string jsonContent,
|
||||
Uri tokenEndpoint,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var body = new
|
||||
{
|
||||
appId = authApp.AppId,
|
||||
appSecret = authApp.AppSecret,
|
||||
accessCode = accessCode,
|
||||
challenge = challenge,
|
||||
codeVerifier = codeVerifier,
|
||||
};
|
||||
|
||||
using StringContent content = new(JsonConvert.SerializeObject(body, _serializerSettings));
|
||||
using StringContent content = new(jsonContent);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
|
||||
using HttpResponseMessage response = await client
|
||||
@@ -282,6 +282,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
|
||||
return JsonConvert.DeserializeObject<TokenExchangeResponse>(read, _serializerSettings).NotNull();
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static string GenerateCodeVerifier()
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
@@ -296,6 +297,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
|
||||
return Base64UrlEncode(codeVerifierData);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public static string GenerateCodeChallenge(string codeVerifier)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
@@ -312,6 +314,7 @@ public sealed class AuthFlow(ISdkActivityFactory activityFactory, ISpeckleHttp s
|
||||
return Base64UrlEncode(challengeData);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static string Base64UrlEncode(
|
||||
#if NET8_0_OR_GREATER
|
||||
ReadOnlySpan<byte> bytes
|
||||
|
||||
@@ -65,7 +65,12 @@ public sealed class DiskStore
|
||||
|
||||
await foreach (var item in _channel.ReadAllAsync(_cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await writer.WriteLineAsync($"{item.Id}\t{item.SpeckleType}\t{item.Json}").ConfigureAwait(false);
|
||||
await writer.WriteAsync(item.Id).ConfigureAwait(false);
|
||||
await writer.WriteAsync('\t').ConfigureAwait(false);
|
||||
await writer.WriteAsync(item.SpeckleType).ConfigureAwait(false);
|
||||
await writer.WriteAsync('\t').ConfigureAwait(false);
|
||||
await writer.WriteAsync(item.Json.Value).ConfigureAwait(false);
|
||||
await writer.WriteLineAsync().ConfigureAwait(false);
|
||||
}
|
||||
#if NET8_0_OR_GREATER
|
||||
await writer.FlushAsync(_cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user