Compare commits

..

1 Commits

Author SHA1 Message Date
Jedd Morgan 7f50987201 Auth flow updates (#464)
.NET Build and Publish / build (push) Has been cancelled
* Auth flow updates

* remove obsolete flag on refresh
2026-03-26 14:42:21 +01:00
2 changed files with 38 additions and 68 deletions
+8 -41
View File
@@ -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)
+30 -27
View File
@@ -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