DPoP Handshake with Inrupt Identity Server Fails When Connecting to Solid Pods #131
Unanswered
grootstebozewolf
asked this question in
Open Source
Replies: 2 comments
|
We don't have experience with Inrupt Solid Pods. But we can see if we can help if more details are provided.
|
0 replies
|
Hi Roland,
I wanted to share an update on an issue I recently encountered and resolved
regarding client configuration for DPoP. The crux of the problem was
twofold:
1. *Correct ALG and Scope:* Ensuring that the right algorithm (“ES256”) and
scope were applied.
2. *Understanding HTU and HTI:* Gaining clarity on how the HTU (HTTP URI)
and HTI (HTTP Method) values work within the client configuration.
To help illustrate the solution, I refactored the code into a more
digestible format with clear sections and detailed comments. Please find
the updated code attached below.
I believe this approach not only resolves the issue but also improves
maintainability. I’m curious to hear your thoughts or suggestions for
further improvements.
Best regards,
Jeroen Bloemscheer
Code used 1. HttpClientAbstractFactory
```csharp
using IHL.Connectoren.Application.Services.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace IHL.Connectoren.Infrastructure
{
/// <summary>
/// A simple abstract factory to create HttpClient instances configured via a client strategy.
/// </summary>
public class HttpClientAbstractFactory : IHttpClientAbstractFactory
{
private readonly IServiceProvider _serviceProvider;
public HttpClientAbstractFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task<HttpClient> CreateClient(IClientConfigurationStrategy strategy)
{
// Create a new service collection to register dependencies
var services = new ServiceCollection();
// For demonstration purposes, use an in-memory cache.
// TODO: Replace with a distributed cache (e.g., Redis, Azure equivalent) for production.
services.AddDistributedMemoryCache();
// Register the client credentials token management service.
services.AddClientCredentialsTokenManagement()
.AddClient("solid_pod_client", strategy.Configure);
// Configure an HttpClient that uses client credentials.
services.AddClientCredentialsHttpClient("client", "solid_pod_client", client =>
{
client.BaseAddress = new Uri("https://storage.inrupt.com");
});
// Build the service provider and resolve the HttpClientFactory.
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
return Task.FromResult(factory.CreateClient("client"));
}
}
}
```
2. SolidPodClientConfiguration
```csharp
using System.Security.Cryptography;
using System.Text.Json;
using Duende.AccessTokenManagement;
using Duende.IdentityModel.Client;
using IHL.Connectoren.Application.Services.Interfaces;
using Microsoft.IdentityModel.Tokens;
namespace IHL.Connectoren.Infrastructure
{
/// <summary>
/// Client configuration for Solid Pod interactions.
/// </summary>
public class SolidPodClientConfiguration : IClientConfigurationStrategy
{
private static readonly ECDsa SharedKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
private readonly string _identityProvider;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string? _scope;
public Uri Address { get; private init; }
public SolidPodClientConfiguration(
string identityProvider,
string clientId,
string clientSecret,
string scope,
Uri address)
{
_identityProvider = identityProvider;
_clientId = clientId;
_clientSecret = clientSecret;
_scope = scope;
Address = address;
}
public void Configure(ClientCredentialsClient client)
{
// Retrieve the discovery document to obtain the token endpoint.
using var httpClient = new HttpClient();
var discoResponse = httpClient.GetDiscoveryDocumentAsync(_identityProvider).Result;
// Set client parameters.
client.TokenEndpoint = discoResponse.TokenEndpoint;
client.ClientId = _clientId;
client.ClientSecret = _clientSecret;
client.Scope = _scope ?? discoResponse.ScopesSupported.FirstOrDefault();
// Configure DPoP by exporting the public key as a JWK.
client.DPoPJsonWebKey = JsonSerializer.Serialize(ExportPublicKeyToJwk(SharedKey));
}
/// <summary>
/// Exports an ECDsa public key as a JSON Web Key (JWK).
/// </summary>
private static Dictionary<string, object> ExportPublicKeyToJwk(ECDsa key)
{
var parameters = key.ExportParameters(true);
string x = Base64UrlEncoder.Encode(parameters.Q.X)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
string y = Base64UrlEncoder.Encode(parameters.Q.Y)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
string d = Base64UrlEncoder.Encode(parameters.D)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
return new Dictionary<string, object>
{
{ "kty", "EC" },
{ "kid", Guid.NewGuid() },
{ "alg", "ES256" },
{ "crv", "P-256" },
{ "x", x },
{ "y", y },
{ "d", d }
};
}
}
}
```
3. SolidPods
```csharp
using System.Net;
using IHL.Connectoren.Application.Services.Interfaces;
using Microsoft.Extensions.Logging;
using VDS.RDF;
using VDS.RDF.Parsing;
namespace IHL.Connectoren.Infrastructure
{
/// <summary>
/// Service to interact with Solid Pods: retrieve storage info and post files.
/// </summary>
public class SolidPods : ISolidPods
{
private readonly ILogger<SolidPods> _logger;
private readonly IHttpClientAbstractFactory _httpClientAbstractFactory;
public SolidPods(ILogger<SolidPods> logger, IHttpClientAbstractFactory httpClientAbstractFactory)
{
_logger = logger;
_httpClientAbstractFactory = httpClientAbstractFactory;
}
/// <summary>
/// Retrieves the root container (storage URL) from the Solid Pod profile using the given WebID.
/// </summary>
public async Task<string?> GetRootContainerFromWebIdAsync(string webId, IClientConfigurationStrategy clientConfigurationStrategy)
{
// Create an HttpClient with the provided client configuration.
var client = await _httpClientAbstractFactory.CreateClient(clientConfigurationStrategy);
// Set the Accept header to request JSON‑LD format.
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json+ld"));
HttpResponseMessage response;
try
{
// Send a GET request to the WebID URL.
response = await client.GetAsync(webId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving profile from WebID: {WebId}", webId);
return null;
}
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Failed to retrieve profile from WebID: {WebId}. Status code: {StatusCode}",
webId, response.StatusCode);
return null;
}
// Read and parse the JSON‑LD content.
var jsonLdContent = await response.Content.ReadAsStringAsync();
ITripleStore store = new TripleStore();
var parser = new JsonLdParser();
try
{
using var sr = new StringReader(jsonLdContent);
parser.Load(store, sr);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing JSON‑LD from WebID: {WebId}", webId);
return null;
}
// Retrieve the default graph from the store.
var graph = store.Graphs.FirstOrDefault();
if (graph == null)
{
_logger.LogError("No graph loaded from JSON‑LD content for WebID: {WebId}", webId);
return null;
}
// Define the storage predicate as per the Solid specification.
var storagePredicateUri = "http://www.w3.org/ns/pim/space#storage";
INode webIdNode = graph.CreateUriNode(UriFactory.Create(webId));
INode storagePredicate = graph.CreateUriNode(UriFactory.Create(storagePredicateUri));
// Extract the storage triple.
var storageTriple = graph.GetTriplesWithSubjectPredicate(webIdNode, storagePredicate).FirstOrDefault();
if (storageTriple == null)
{
_logger.LogError("No storage property found in the profile for WebID: {WebId}", webId);
return null;
}
// Return the storage value, handling both URI and literal cases.
return storageTriple.Object is IUriNode uriNode
? uriNode.Uri.AbsoluteUri
: storageTriple.Object is ILiteralNode literalNode
? literalNode.Value
: storageTriple.Object.ToString();
}
/// <summary>
/// Posts a file to the specified Solid Pod.
/// </summary>
public async Task<bool> PostAsync(string podUrl, string fileName, byte[] fileBytes, IClientConfigurationStrategy clientConfigurationStrategy)
{
try
{
// Prepare the file content.
var byteArrayContent = new ByteArrayContent(fileBytes);
byteArrayContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain");
byteArrayContent.Headers.Add("Slug", fileName);
// Create a configured HttpClient.
var httpClient = await _httpClientAbstractFactory.CreateClient(clientConfigurationStrategy);
var createResponse = await httpClient.PostAsync(podUrl, byteArrayContent);
if (createResponse.IsSuccessStatusCode)
{
return true;
}
else
{
var errorContent = await createResponse.Content.ReadAsStringAsync();
int statusCode = (int)createResponse.StatusCode;
if (createResponse.StatusCode == HttpStatusCode.Conflict)
{
_logger.LogInformation("The file '{FileName}' already exists on the Solid Pod at '{PodUrl}'.", fileName, podUrl);
}
else if (statusCode >= 400 && statusCode < 500)
{
_logger.LogError("Posting data to Solid Pod failed: {StatusCode} - {ErrorContent}",
createResponse.StatusCode, errorContent);
}
else if (statusCode >= 500)
{
_logger.LogWarning("Solid Pod might be temporarily unavailable: {StatusCode} - {ErrorContent}",
createResponse.StatusCode, errorContent);
}
else
{
_logger.LogInformation("Unknown state: {StatusCode} - {ErrorContent}",
createResponse.StatusCode, errorContent);
}
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Posting data to Solid Pod failed: {Message}", ex.Message);
return false;
}
}
}
}
```
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Title: DPoP Handshake with Inrupt Identity Server Fails When Connecting to Solid Pods
Environment:
Description:
I'm using the Duende IdentityManagement Client to obtain an access token from Inrupt’s Identity Server with DPoP enabled. The access token is successfully retrieved; however, when I add the DPoP signature for the handshake with Inrupt’s Solid Pods, the token gets rejected.
For context, DPoP (Demonstration of Proof-of-Possession) is a security mechanism that binds an access token to the client by including a JSON Web Key (JWK) and several claims (e.g., HTTP method, URL, issued-at timestamp, unique identifier) in a JWT. If any of these elements (or the signing process) are misconfigured, the proof token may not be accepted.
Reproduction Steps:
Expected Behavior:
The Solid Pod should accept the token, indicating a successful DPoP handshake with a correctly generated DPoP proof.
Logs:
No detailed error logs are available—the failure is observed as the token being rejected. When using the same client code configured to local IDP and API as in this example, it works correctly.
Additional Context:
Questions:
All reactions