Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve Users Host PDS before authentication #126

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/FishyFlip/ATProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public async Task<string> GenerateOAuth2AuthenticationUrlAsync(string clientId,
/// <param name="handle"><see cref="ATHandle"/>.</param>
/// <param name="token">Cancellation Token.</param>
/// <returns>String of Host URI if it could be resolved, null if it could not.</returns>
public async Task<string?> ResolveATHandleHostAsync(ATHandle handle, CancellationToken? token = default)
public async Task<Result<string?>> ResolveATHandleHostAsync(ATHandle handle, CancellationToken? token = default)
{
string? host = this.options.DidCache.FirstOrDefault(n => n.Key == handle.ToString()).Value;
if (!string.IsNullOrEmpty(host))
Expand All @@ -259,7 +259,7 @@ public async Task<string> GenerateOAuth2AuthenticationUrlAsync(string clientId,

try
{
var endpointUrl = $"{Constants.Urls.ATProtoServer.SocialApi}{IdentityEndpoints.ResolveHandle}?handle={handle}";
var endpointUrl = $"{Constants.Urls.ATProtoServer.PublicApi}{IdentityEndpoints.ResolveHandle}?handle={handle}";
var result = await this.Client.GetAsync(endpointUrl, token ?? CancellationToken.None);
if (result.IsSuccessStatusCode)
{
Expand All @@ -268,7 +268,7 @@ await result.Content.ReadAsStringAsync(),
this.options.SourceGenerationContext.ComAtprotoIdentityResolveHandleOutput);
if (resolveHandle?.Did is not null)
{
host = await this.ResolveATDidHostAsync(resolveHandle.Did, token);
(host, var error) = await this.ResolveATDidHostAsync(resolveHandle.Did, token);
if (!string.IsNullOrEmpty(host))
{
this.options.DidCache[handle.ToString()] = host!;
Expand All @@ -277,6 +277,7 @@ await result.Content.ReadAsStringAsync(),
else
{
this.options.Logger?.LogError($"Failed to resolve Handle: {handle}, missing Service Handle.");
return error;
}
}
else
Expand All @@ -286,6 +287,15 @@ await result.Content.ReadAsStringAsync(),
}
else
{
var resolveError = JsonSerializer.Deserialize<ATError>(
await result.Content.ReadAsStringAsync(),
this.options.SourceGenerationContext.ATError);
if (resolveError is not null)
{
this.options.Logger?.LogError($"Failed to resolve Handle: {handle}. {resolveError}");
return resolveError;
}

this.options.Logger?.LogError($"Failed to resolve Handle: {handle}. {result.StatusCode}");
}
}
Expand All @@ -303,7 +313,7 @@ await result.Content.ReadAsStringAsync(),
/// <param name="did"><see cref="ATDid"/>.</param>
/// <param name="token">Cancellation Token.</param>
/// <returns>String of Host URI if it could be resolved, null if it could not.</returns>
public async Task<string?> ResolveATDidHostAsync(ATDid did, CancellationToken? token = default)
public async Task<Result<string?>> ResolveATDidHostAsync(ATDid did, CancellationToken? token = default)
{
string? host = this.options.DidCache.FirstOrDefault(n => n.Key == did.ToString()).Value;
if (!string.IsNullOrEmpty(host))
Expand All @@ -317,7 +327,12 @@ await result.Content.ReadAsStringAsync(),
switch (did.Type)
{
case "plc":
host = await this.ResolvePlcDidAsync(did, token);
(host, var error) = await this.ResolvePlcDidAsync(did, token);
if (error is not null)
{
return error;
}

break;
case "web":
host = await this.ResolveWebDidAsync(did, token);
Expand Down Expand Up @@ -366,7 +381,7 @@ private void OnSessionUpdated(object? sender, SessionUpdatedEventArgs e)
this.SessionUpdated?.Invoke(sender, e);
}

private async Task<string?> ResolvePlcDidAsync(ATDid did, CancellationToken? token)
private async Task<Result<string?>> ResolvePlcDidAsync(ATDid did, CancellationToken? token)
{
string? host = null;
if (this.IsAuthenticated && this.Session?.Did.ToString() == did.ToString())
Expand Down Expand Up @@ -397,6 +412,7 @@ private void OnSessionUpdated(object? sender, SessionUpdatedEventArgs e)
else
{
this.options.Logger?.LogError($"Failed to resolve plc DID: {did}. {error?.ToString()}");
return error;
}

return host;
Expand Down
52 changes: 52 additions & 0 deletions src/FishyFlip/PasswordSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,52 @@ public Task RefreshSessionAsync(CancellationToken cancellationToken = default)
/// <returns>A Task that represents the asynchronous operation. The task result contains a Result object with the session details, or null if the session could not be created.</returns>
internal async Task<Result<Session?>> CreateSessionAsync(string identifier, string password, string? authFactorToken = default, CancellationToken cancellationToken = default)
{
var host = this.protocol.Options.Url.ToString();
var usingPublicApi = host.Contains(Constants.Urls.ATProtoServer.PublicApi);
if (usingPublicApi)
{
this.logger?.LogInformation($"Using public server {host} to login, resolving identifier to find host.");
var emailRegex = new Regex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$");
if (emailRegex.IsMatch(identifier))
{
this.logger?.LogWarning($"Using email address {identifier} to login to {host}. This is not recommended. Please use your handle instead.");
}
else if (ATIdentifier.TryCreate(identifier, out var ident))
{
string? resolveHost = null;
if (ident is ATHandle handle)
{
(resolveHost, var resolveHandleError) = await this.protocol.ResolveATHandleHostAsync(handle, cancellationToken);
if (resolveHandleError is not null)
{
this.logger?.LogError($"Error resolving handle {identifier}: {resolveHandleError}");
}
}
else if (ident is ATDid did)
{
(resolveHost, var resolveDidError) = await this.protocol.ResolveATDidHostAsync(did, cancellationToken);
if (resolveDidError is not null)
{
this.logger?.LogError($"Error resolving did {identifier}: {resolveDidError}");
}
}

if (Uri.TryCreate(resolveHost, UriKind.Absolute, out var uri))
{
this.protocol.Options.Url = uri;
this.logger?.LogInformation($"Resolved handle {identifier} to {uri}, setting for authentication");
}
else
{
this.logger?.LogWarning($"Could not resolve identifier {identifier} to a valid host.");
}
}
else
{
this.logger?.LogWarning($"Could not validate identifier: {identifier}");
}
}

#pragma warning disable CS0618
var (session, error) = await this.protocol.CreateSessionAsync(identifier, password, authFactorToken, cancellationToken: cancellationToken);
#pragma warning restore CS0618
Expand Down Expand Up @@ -147,6 +193,12 @@ public Task RefreshSessionAsync(CancellationToken cancellationToken = default)
if (error is not null)
{
this.logger?.LogError(error.ToString());
if (usingPublicApi)
{
this.logger?.LogInformation($"Login failed, Resetting to public server {Constants.Urls.ATProtoServer.PublicApi}.");
this.protocol.Options.Url = new Uri(Constants.Urls.ATProtoServer.PublicApi);
}

return error;
}

Expand Down
10 changes: 5 additions & 5 deletions src/FishyFlip/Tools/ATProtocolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public static async Task<Result<TK>> Post<T, TK>(
private static async Task<string> ResolveHostUri(this ATProtocol protocol, string pathAndQueryString)
{
var logger = protocol.Options.Logger;
string host = string.Empty;
string? host = null;

// Find repo name in pathAndQueryString
if (pathAndQueryString.Contains("repo"))
Expand All @@ -165,11 +165,11 @@ private static async Task<string> ResolveHostUri(this ATProtocol protocol, strin
var repo = repoName.Split('=')[1];
if (ATDid.TryCreate(repo, out ATDid? did))
{
host = await protocol.ResolveATDidHostAsync(did!) ?? string.Empty;
(host, _) = await protocol.ResolveATDidHostAsync(did!);
}
else if (ATHandle.TryCreate(repo, out ATHandle? handle))
{
host = await protocol.ResolveATHandleHostAsync(handle!) ?? string.Empty;
(host, _) = await protocol.ResolveATHandleHostAsync(handle!);
}
}
}
Expand All @@ -182,7 +182,7 @@ private static async Task<string> ResolveHostUri(this ATProtocol protocol, strin
var did = didName.Split('=')[1];
if (ATDid.TryCreate(did, out ATDid? atdid))
{
host = await protocol.ResolveATDidHostAsync(atdid!) ?? string.Empty;
(host, _) = await protocol.ResolveATDidHostAsync(atdid!);
}
}
}
Expand Down Expand Up @@ -214,7 +214,7 @@ private static async Task<string> ResolveHostUri(this ATProtocol protocol, strin
throw new InvalidOperationException("Host is required.");
}

if (host.EndsWith("/") && pathAndQueryString.StartsWith("/"))
if (host!.EndsWith("/") && pathAndQueryString.StartsWith("/"))
{
host = host[0..^1];
}
Expand Down
2 changes: 2 additions & 0 deletions website/docs/logging-in.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Console.WriteLine($"Session Handle: {session.Handle}");
Console.WriteLine($"Session Token: {session.AccessJwt}");
```

If you don't override the Instance URL in `ATProtocolBuilder.WithInstanceUrl` the users PDS Host will be resolved before the authentication attempt is made and will be used for authentication and future requests. If you have set it, the authentication request will be resolved against that endpoint.

- OAuth authentication is more complex. There is a full example showing a [local user authentication session](https://github.com/drasticactions/BSkyOAuthTokenGenerator/tree/main/src/BSkyOAuthTokenGenerator) but in short, you must:
- Starting the session with `atProtocol.GenerateOAuth2AuthenticationUrlAsync`
- Sending the user to a web browser to log in
Expand Down
Loading