Unable to Successfully Authenticate with Custom Solid OIDC Server in Client App

Hello,

I’m developing a Solid application using the @inrupt/solid-client-authn-browser library and attempting to authenticate with my own Solid OIDC server instance hosted at:

https://gardenmap.jacobdaniel.co.uk/

I manage this server myself (Community Solid Server – CSS) and have successfully:

  • Created a user and Pod.

  • Logged into 3rd-party Solid apps using this server, which confirms the server works and issues tokens correctly.


What I’m Trying to Do

Set up a basic login flow in a browser app using:

await login({ clientId: "...", clientSecret: "...", // also tried PKCE-only login without secret redirectUrl: "http://localhost:3003/callback", oidcIssuer: "https://gardenmap.jacobdaniel.co.uk/", scopes: ["openid", "profile", "offline_access"], });

Followed by:

await handleIncomingRedirect(window.location.href);

Goal: Authenticate and retrieve a WebID via getDefaultSession().info.webId.

Problem - the login flow fails at various stages depending on configuration. Issues observed:

  • login() call sometimes fails with:

    • invalid_redirect_uri or

    • invalid_client errors.

  • .oidc/reg endpoint responds with 400 despite valid payload.

  • When testing login manually (e.g. via cURL), I receive a client ID and secret, but the app still fails to complete login via SDK.

  • In handleIncomingRedirect(), session.info.isLoggedIn remains false, and session.info.webId is undefined.

  • GET requests to .oidc/reg in browser return “Resource Not Found”.

Despite all this, I am able to authenticate using this same server in other Solid apps, so the server itself seems functional.


What I have tried

  1. Successfully registered a client with POST to /.oidc/reg:

    • Received client_id, client_secret, and confirmation of redirect_uris.
  2. Confirmed proper authorization code flow:

    • Redirect with code, state, and iss is returned from the server.
  3. Used session.handleIncomingRedirect():

    • But session reports isLoggedIn = false.
  4. Tested both PKCE and client_secret-based flows.

  5. Logged in successfully from another Solid App using this same server.

  6. Manually verified token response via curl and verified correct WebID issued.

  7. Inspected logs on server for:

    • Missing ACLs.

    • Successful DPoP verification when used via 3rd-party app.


Thoughts

  • Something in the login flow config (client app side) is not aligning with what the Solid server expects.

  • Possibly session state isn’t persisting across the login + callback flow in the app.

  • Could also be an issue with dynamic client registration handling vs manual registration.


Help

  • Is there a recommended pattern or minimal working example to authenticate against a custom CSS server from a browser app?

  • How can I confirm that handleIncomingRedirect() is failing due to a specific error?

  • Do I need to handle registration differently when using @inrupt/solid-client-authn-browser?

  • Is it possible to skip the SDK’s login() and handle the OIDC steps manually (e.g. via fetch / URL manipulation)?

Hello.

You asked a lot here so I will try and keep it short. The library you used is meant to work with Inrupt servers. They do some things differently than the CSS approach. I think in your login request you must request the ‘webid’ scope in order to receive the webid claim. That is probably why session.info.webId is undefined.

@gaz009 I don’t think that that is true - I have successfully used it to log in to non-Inrupt servers.

@jacobdaniel You have confirmed that the server works because you can use other apps with it, but it might be worth confirming the other side as well: does your app work with different servers? For example, can you log in to https://solidcommunity.net with it?

I would at first try it without a client ID, to ensure it’s not broken because the client ID is invalid.

I also see you’re passing the arguments clientSecret and scopes, which I don’t see listed in the docs. If you’re using generative AI to guide you, maybe best not to trust its suggestions, unless you verify it yourself in the docs. There also shouldn’t be a need to manually POST to anywhere - if you are using an AI, it sounds like it’s trying to mimic non-Solid login flows.

Also make sure that the redirect URL you’re pointing at (above, http://localhost:3003/callback) is the page where you’re calling handleIncomingRedirect.

If that’s not enough to get you there, it might be worth working through the docs I linked through above, see if you can at least get that to work?

1 Like

I think Vincent’s suggestions make a lot of sense. The library does work on other Solid server implementations, not just Inrupt’s.

Using it shouldn’t be complicated at all. In fact, the simplest way is to only pass three arguments to the login() function, as shown here in the document:

    await login({
      oidcIssuer: "https://login.inrupt.com",
      redirectUrl: new URL("/callback", window.location.href).toString(),
      clientName: "My application"
    });

You passed many more arguments, and most of them I have never seen. I understand they may be needed eventually for the OIDC flow, but unnecessary (and potentially won’t work, as Vincent pointed out) for using this library as it will handle those things behind the scene.

Also, don’t forget to call handleIncomingRedict() in a sensible place. It’s not physically after the login() call, but logically after the login() call. This is because how OIDC and webpages works – it firsts redirects you to the OIDC issuer’s site, and redirect to your original app (webpage); but this second time you open your webpage, it starts from the beginning, rather than “resuming” from where you called login().

1 Like

Thanks all, and apologies for the messy post, I am picking up from my attempts at creating Solid App a few weeks ago, and indeed carelessly copied error debugging from AI assistance.

The docs for authenticating a user on a solid app with a community solid server do seem straight forward.

But I am still getting 400 bad request. The payload shows “redirect_uris”: [null]

"use client";
import React, { useEffect, useState } from "react";
import {
  login,
  getDefaultSession,
} from "@inrupt/solid-client-authn-browser";

export default function Home() {
  const [solidServer] = useState("https://gardenmap.jacobdaniel.co.uk");

  const [webId, setWebId] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const session = getDefaultSession();
    if (session.info.isLoggedIn) {
      setWebId(session.info.webId);
      setLoading(false);
    } else {
      setLoading(false);
    }
  }, []);

  const handleLogin = async () => {
    const callbackUrl = new URL("/callback", window.location.origin).toString();
    await login({
      oidcIssuer: solidServer,
      redirectUrl: callbackUrl,
      clientName: "My Solid App",
    });
  };

  return (
    <div>
      {loading && <p>Loading...</p>}

      {!loading && webId && (
        <>
          <p>Logged in as: {webId}</p>
        </>
      )}

      {!loading && !webId && (
        <>
          <p>Not logged in</p>
          <button
            className="border bg-white cursor-pointer p-2 py-1 rounded"
            onClick={handleLogin}
          >
            Login
          </button>
        </>
      )}
    </div>
  );
}

Solid App

It’s really hard to analyse that since there are some syntax errors in there, but it looks like your callback URL /callback doesn’t actually exist. As I understand it, you’re all just running it on the same page, / (aka https://solidpod.jacobdaniel.co.uk). So maybe instead of callbackUrl you just want to pass in window.location.href, i.e. since the page that has the call to login also has the handleIncomingRedirect, you just want to pass in the current page’s URL?

You’re quite right, I had the login and redirect handling on the same page (/), I’ve now separated concerns.

My /callback :

"use client";
import { useEffect, useState } from "react";
import {
  handleIncomingRedirect,
  getDefaultSession,
} from "@inrupt/solid-client-authn-browser";
import { getSolidDataset, getThing, getIri } from "@inrupt/solid-client";
import Link from "next/link";

export default function CallbackPage() {
  const [webId, setWebId] = useState(null);
  const [podRoot, setPodRoot] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function completeLogin() {
      await handleIncomingRedirect();
      const session = getDefaultSession();
      if (session.info.isLoggedIn) {
        setWebId(session.info.webId);

        try {
          const profileDoc = await getSolidDataset(session.info.webId);
          const profile = getThing(profileDoc, session.info.webId);
          const podRoot = getIri(
            profile,
            "http://www.w3.org/ns/pim/space#storage",
          );
          setPodRoot(podRoot);
        } catch (err) {
          console.error("Error loading profile data:", err);
        }
      }

      setLoading(false);
    }

    completeLogin();
  }, []);

  return (
    <div>
      <h1>Callback Page</h1>
      {loading && <p>Loading...</p>}

      {!loading && webId && (
        <>
          <p>Logged in as: {webId}</p>
          {podRoot && <p>Pod Root: {podRoot}</p>}
        </>
      )}

      {!loading && !webId && (
        <div>
          <p>Not logged in.</p>
          <Link href="/" className="text-blue-400">
            Home
          </Link>
        </div>
      )}
    </div>
  );
}

Response:

{
    "name": "InvalidClientMetadata",
    "message": "invalid_redirect_uri",
    "statusCode": 400,
    "errorCode": "H400",
    "details": {},
    "error": "invalid_redirect_uri",
    "error_description": "redirect_uris must only contain strings"
}

OK, if I’m following correctly, that’s what your Solid server returns before you’re sent back to /callback - and it seems to say that you’re passing an invalid redirect URL.

Could you add a log just before your call to login, showing the value that you’re passing to its redirectUrl parameter?

Yes. I understand that the redirectUrl needs to be a proper, fully qualified string, so I have tested using both hardcoded string and dynamic URL object, both logs just before login() is called appear as clickable links:

callback url: http://localhost:3002/callback 
solid server url: https://gardenmap.jacobdaniel.co.uk/

Additionally, in my /callback page I log:

 console.log("Before redirect handling:", window.location.href);
 await handleIncomingRedirect();
 console.log("After redirect handling:", window.location.href);

Console:

Before redirect handling: http://localhost:3002/callback?code=hvu02XVxCdzv3_q4hY7V285PTn1hQnt6Quoxfox6H7D&state=5bf56dff4d06464290efa79b910a6e68&iss=https%3A%2F%2Fgardenmap.jacobdaniel.co.uk%2F
page.jsx:19 
After redirect handling: http://localhost:3002/callback

This suggests I am providing the redirectUrl value correctly and makes me wonder why I get response error?

I have tried setting the oidcIssuer to https://solidcommunity.net server and get the same error.

Hmm yeah, that all looks OK. Could you tell us which URL is sending that InvalidClientMetadata response that you posted above?

Both server URLs l mentioned, solidcommunity.net and my personal server show that response.

We’ll need the full URL to be able to say anything meaningful though - I want to know which step in the authentication flow it is, and what is being communicated between the app and the Pod :slight_smile:

1 Like

Network during login:

When I log in using both https://solidcommunity.net and my own server https://gardenmap.jacobdaniel.co.uk, I see a successful response to the /.oidc/reg request with status 201 Created. Here’s the payload being sent:

{
    "client_name": "My Solid App",
    "application_type": "web",
    "redirect_uris": [
        "http://localhost:3002/callback"
    ],
    "subject_type": "public",
    "token_endpoint_auth_method": "client_secret_basic",
    "id_token_signed_response_alg": "ES256",
    "grant_types": [
        "authorization_code",
        "refresh_token"
    ]
}

So registration doesn’t appear to be the issue. However, after I approve the login on the CSS (Pivot) interface, I’m redirected to /callback and that’s where I see the InvalidClientMetadata error in the page response.

I previously posted the URL before and after calling handleIncomingRedirect():

This happens with both servers.

I thought the issue could be related to my use of http://localhost for the app, given that my CSS is on https, but I get the same response on my own https hosted solidpod domain.

Hmm, I think I’m misunderstanding. Would it be possible to share a screenshot of your browser window when you’re seeing the error response?

Sure, really appreciate your help with this!

Enabling “Preserve log” in the devtools has helped trace the network activity through the full auth

My solidpod domain is currently configured to authenticate with my hosted CSS in case you are able to test for yourself.

https://solidpod.jacobdaniel.co.uk

That’s weird, it’s making a bunch of requests that I don’t see in my app, including the one to /reg that errors out. You don’t happen to have the full source code you’re using posted somewhere, by any chance? Nothing jumps out to me as the obvious cause, unfortunately.

1 Like

Thanks for taking a look!

Well, I checked out your code and played around a bit, and then I noticed that you’re using an older version of @inrupt/solid-client-authn-browser. Upgrading that to the latest one allowed me to log in successfully. So you’re probably hitting a bug that was since fixed in that library.

(Also, apparently it had a whole slew of releases that I wasn’t aware of, so my knowledge of it is probably outdated anyway :sweat_smile:)

1 Like

Oh dear… that’s slightly annoying - I had run pnpm update but didn’t realise it wouldn’t upgrade to a newer major version without explicitly specifying it. I see pnpm update <package> is more reliable.

Anyway, I am familiar with the login flow now :face_with_bags_under_eyes:

Thank you for going over it with me Vincent!