How I debug Solid client libraries
SolidJavascriptBeing an engineer at Inrupt, I write code interacting with components of the Solid ecosystem (server or apps) for a living. This means I have spent my fair share of time scratching my head with a puzzled look on my face while the computer wasn't doing what I intended, an activity also known as "debugging".
I am not going to talk here about traditional debuggers, with breakpoint and state analysis, because there are plenty of resources out there about them, and I'm not a particularly proficient user. The issues I am talking about here don't require to go step-by-step into the codebase I control, but rather, to understand the flow of data going back and forth between my client and the server it is talking to. This is in reality a generic issue to Web apps overall, applicable way beyond Solid, but Solid-related things is what I happen to debug most of the time.
In particular, the issues I am usually looking into are along the lines of: my client is not behaving as I thought it would, and either it is hitting an error response from the server (typically 400 Bad Request, 401 Unauthorized, or 403 Not Allowed), or it is getting an unexpected success response.
The question is then: what exactly did I send to the server to get that response? When I'm writing library code, the answer to that question is not always as obvious as I'd want.
Testing directly in Javascript #
There are two ways to test a Javascript client library against a live server (provided the library is isomorphic):
- Using the library in a Web app
- using the library in a NodeJS script
In the browser #
In the case of an in-browser Web app (for instance, a Single Page App), the browser
network inspector shows details about individual requests and responses, and even
allows to replay a request changing some parameters. In most cases, authentication
is required to my use cases, which in the browser involves a redirect to the OpenID
Provider, and a redirect back to the app, which is why my script template imports
@inrupt/solid-client-authn-browser
. Here is the typical setup I use:
- A (very) basic
index.html
file
<html>
<body>
<p>My test</p>
</body>
<script type="module" src="dist/main.js"></script>
</html>
- A JS module
index.js
, using NPM to handle dependencies
import { Session } from "@inrupt/solid-client-authn-browser";
const session = new Session();
await session.handleIncomingRedirect({ url: window.location });
if (!session.info.isLoggedIn) {
console.log("Logging in");
await session.login({
oidcIssuer: "https://login.inrupt.com",
});
}
// ... do authenticated things.
await session.logout();
- Webpack, to bundle all the dependencies:
npx webpack ./index.js
. This should outputdist/main.js
. serve
to serve the resulting page from my file system:npx serve .
With that setup, I get a JS script running in the browser where I can use all the usual debug tools, and inspect the network traffic, which is really the main point here.
In a NodeJS script #
Compared to a browser script requiring bundling and redirection to the OpenID
Provider, a NodeJS script is pretty straightforward because it can use the Client
Credentials flow for authentication, which means you don't need redirection, and
can provide credentials directly through environment variables (using the recent
Node addition --env-file
, no extra dependency required).
The main issue with NodeJS being the absence of a native network capture tool
such as the network console of the browser developer tools. However, since version
18 of NodeJS, a native fetch
implementation is available, provided by undici
.
This fetch
supports global settings, in particular for a proxy dispatcher. It
is then possible to have all fetch
-based network interaction go through a
local debug proxy (such as mitmpoxy) to get the same
experience as the browser console. An obvious limitation is that network traffic
using a different http client library will not be captured: each library has its
own configuration.
- The NodeJS script, using NPM to manage dependencies. This example is an ES module, but a CJS module would work similarly:
import { Session } from "@inrupt/solid-client-authn-node";
import { ProxyAgent, setGlobalDispatcher } from 'undici'
const proxyAgent = new ProxyAgent('http://localhost:8080');
setGlobalDispatcher(proxyAgent)
const session = new Session();
await session.login({
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
oidcIssuer: process.env.OP
});
// ... do authenticated things.
await session.logout();
- Running the proxy:
docker run --rm -it -p 8080:8080 mitmproxy/mitmproxy
.mitmproxy
provides a docker image, so if you have docker installed, there is nothing additional to setup. - Sample .env file:
# Go to https://login.inrupt.com/registration.html to get client credentials.
CLIENT_ID="..."
CLIENT_SECRET="..."
OP="https://login.inrupt.com"
# Required when proxying HTTPS requests to an HTTP proxy
NODE_TLS_REJECT_UNAUTHORIZED=0
- Running the NodeJS script setting the environment:
node --env-file .env index.js
.
Testing using a "rich API client" #
A "rich API client" is a REST client used to test APIs, typically Postman or Bruno. Both of these support OAuth 2.0, and in particular the Client Credentials flow, so they can both be used to issue authenticated requests to a server.
Compared to testing directly the faulty library in Javascript, using an API client makes it really reasy to iterate rapidly and finely tweak the sent HTTP requests. That is great for testing single endpoint request/response. The downside is that sequences of interactions are harder to get to, each rich client having its own scripting API.
Summary #
Method | Strenghts | Weaknesses |
---|---|---|
Browser script | Browser debug tools | Requires a bundler |
NodeJS script | No user interaction required | Requires a debug proxy |
Rich API client | Easy to tweak request details | Scripting is client-specific |
Bonus track: Looking into a JWT #
JSON Web Tokens (JWT) are widely used in Solid for authentication. Sometimes, debugging why one has/doesn't have access to a resource requires looking into these tokens to examine its claims. There are online services allowing to parse JWTs, but as they are sensitive pieces of data, you may not want to share them with a third party.
If you are not interested in verifying the JWT signature (trusting that the JWT is properly signed) and only want to read its content, the following NodeJS 1-liner is enough to read the JWT payload:
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJzdWIiOiJ5b3UiLCJjYW5SZWFkIjoiSldUIn0."
+ "fFfj8ocbSSSCqfWxcdp-K72G_Kyku0qIgFus62c5m_Y";
Buffer.from(jwt.split(".")[1], "base64").toString("utf8");