Private
Public Access
1
0

Fix SimpleFIN integration for beta-bridge.simplefin.org

HttpClient does not extract Basic Auth credentials from URLs with
embedded user:pass@host format. Parse the access URL, build the
Authorization header explicitly, and strip credentials from the
request URI. Also send Content-Length: 0 on token exchange POST
and update setup instructions to point to beta-bridge.simplefin.org.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 20:57:02 -05:00
parent 9497812bf5
commit 1db0101b5a
2 changed files with 24 additions and 4 deletions
+1 -1
View File
@@ -263,7 +263,7 @@
<v-card-title>Connect via SimpleFIN</v-card-title>
<v-card-text>
<p class="mb-4">
Visit <strong>simplefin.org/bridge</strong> to get a setup token for your bank, then paste it below.
Visit <strong>beta-bridge.simplefin.org</strong> to get a setup token for your bank, then paste it below.
</p>
<v-text-field
v-model="simpleFinToken"
@@ -1,3 +1,4 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using Purrse.Plugins.Abstractions;
@@ -28,7 +29,7 @@ public class SimpleFinSyncProvider : IAccountSyncProvider
// Decode base64 setup token to get the claim URL
var claimUrl = Encoding.UTF8.GetString(Convert.FromBase64String(setupToken));
var response = await _httpClient.PostAsync(claimUrl, null);
var response = await _httpClient.PostAsync(claimUrl, new StringContent(string.Empty));
response.EnsureSuccessStatusCode();
var accessUrl = await response.Content.ReadAsStringAsync();
@@ -39,7 +40,7 @@ public class SimpleFinSyncProvider : IAccountSyncProvider
{
var accessUrl = configuration.GetValueOrDefault("AccessUrl") ?? string.Empty;
var response = await _httpClient.GetFromJsonAsync<SimpleFinAccountSet>($"{accessUrl}/accounts");
var response = await GetWithAuthAsync($"{accessUrl}/accounts");
if (response == null)
return new List<SyncAccount>();
@@ -60,7 +61,7 @@ public class SimpleFinSyncProvider : IAccountSyncProvider
var startUnix = new DateTimeOffset(startDate).ToUnixTimeSeconds();
var endUnix = new DateTimeOffset(endDate).ToUnixTimeSeconds();
var response = await _httpClient.GetFromJsonAsync<SimpleFinAccountSet>(
var response = await GetWithAuthAsync(
$"{accessUrl}/accounts?start-date={startUnix}&end-date={endUnix}");
if (response == null)
@@ -90,6 +91,25 @@ public class SimpleFinSyncProvider : IAccountSyncProvider
return result;
}
/// <summary>
/// HttpClient does not extract Basic Auth credentials from URLs (user:pass@host).
/// This method parses the URI, builds the Authorization header, and strips credentials from the request URL.
/// </summary>
private async Task<SimpleFinAccountSet?> GetWithAuthAsync(string url)
{
var uri = new Uri(url);
var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder(uri) { UserName = "", Password = "" }.Uri);
if (!string.IsNullOrEmpty(uri.UserInfo))
{
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(uri.UserInfo));
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
}
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SimpleFinAccountSet>();
}
private static string GuessAccountType(string name)
{
var lower = name.ToLowerInvariant();