Skip to content

IndieAuth: support profile scope by discovering author metadata from the user's site #885

Description

@rmdes

Background

scope.js currently declares profile: { supported: false }, and in #534 you explained why:

Indiekit doesn't support profile scope as it has no information about the owner of the Indiekit server. There's a few options I'm considered:

  • Provide this information in Indiekit's configuration file
  • Perform website discovery when authenticating, and provide any author metadata found

I've been running the second of those in a fork for a few months, and it works well enough that I'd like to offer it upstream — but the shape is your call, so an issue before a PR.

What clients see today

Requesting profile scope gets nothing back, which some clients surface as a warning. indiebookclub asks for it, and @aciccarello noticed the gap in #534. It also means the profile URL is the only thing an authentication-only flow can return, even when the user's own site publishes an h-card saying who they are.

indielogin.com requests it too, which seems worth adding because of what that service is: the replacement for indieauth.com, which now carries a deprecation notice pointing at it. When it delegates to a user's own IndieAuth server it builds the authorization request with 'scope' => 'profile':

// app/Provider/IndieAuth.php:17
$authorize = \IndieAuth\Client::buildAuthorizationURL($login_request['authorization_endpoint'], [
  'me' => $login_request['me'],
  'redirect_uri' => getenv('BASE_URL').'redirect/indieauth',
  'client_id' => getenv('BASE_URL').'id',
  'state' => $state,
  'code_verifier' => $code_verifier,
  'scope' => 'profile',
]);

So the scope is asked for by the service the ecosystem is being pointed towards, not only by older clients. Signing in through indielogin.com against an Indiekit endpoint running the fork described below works, with profile among the requested scopes — though to be precise about what that shows: indielogin.com uses the profile URL for identity, so a server that ignores the scope still completes the sign-in. What it demonstrates is a current, actively maintained client requesting profile as a matter of course, rather than the scope being a legacy concern.

What the implementation does

On the token and profile-URL responses, when profile is among the granted scopes:

  1. Fetch the me URL — the authenticated user's own site.
  2. Parse it with microformats-parser and find the representative h-card, preferring one whose u-url matches the profile URL.
  3. Return name, url and photo under profile, per §5.3 Profile Information.

Nothing is configured: the information comes from what the user already publishes. If there's no h-card, or the fetch fails, the response is unchanged — the scope simply yields nothing rather than failing the request.

It touches four files: a new profile.js, the flag in scope.js, and the two controllers that build responses. The form-encoded token response needs the profile object flattened to JSON, since URLSearchParams would otherwise stringify it as [object Object].

Things worth deciding before a PR

  • Discovery vs configuration. This implements discovery. If you'd rather have it in the configuration file, or support both with configuration winning, that changes the shape.
  • Which properties. I return name, url, photo. The specification also allows email, behind the separate email scope — I left that alone since it's a different scope and a different privacy question.
  • Fetching on every token request. Currently unguarded by any cache. Given getClientInformation fetches client_id uncached too, that seemed consistent, but it is another outbound request per authorization.
  • /userinfo. IndieAuth endpoint: Redeem authorization_code for profile information  #534 also mentioned adding that path for the same response. Not implemented; happy to include it if you'd like them together.

The fetch reuses the isFetchableOrigin guard added in #877, since it's the same kind of outbound request to a URL that ultimately came from user input.

Happy to open a PR against whichever shape you prefer, or to leave it if you'd rather this waited for the database work you mentioned.

Activity

  1. paulrobertlloyd commented on Aug 23, 2026

    @paulrobertlloyd
    Collaborator

    Let’s allow providing this information in configuration, falling back to discovery, and leave email for now because, as you say, it’s a different scope.

    We can capture the issue of caching values retrieved via external requests in another issue.

  2. rmdes commented on Aug 23, 2026

    @rmdes
    CollaboratorAuthor

    Good — configuration with discovery as the fallback, and email left alone.

    The shape I'd implement, unless you'd rather it lived elsewhere:

    "@indiekit/endpoint-auth": {
      profile: {
        name: "…",
        url: "…",
        photo: "…",
      },
    },

    Each key optional and independent: what is configured is used as-is, anything absent is filled in from the h-card discovered at me. So configuring only name still takes the photo from the site, and configuring nothing behaves exactly as the discovery-only version does. Nothing configured and no h-card found means the scope yields nothing, as today.

    I'll open the caching issue separately and link it here.

    One loose end you didn't mention: /userinfo. I'll leave it out of this PR to keep it reviewable, and follow up with it after — say the word if you'd rather have them together.

  3. paulrobertlloyd commented on Aug 23, 2026

    @paulrobertlloyd
    Collaborator

    Perfect. And let’s add support for /userinfo as part of this feature.

  4. rmdes commented on Aug 24, 2026

    @rmdes
    CollaboratorAuthor

    @paulrobertlloyd Happy to add it. To be sure I build what you have in mind, here's my reading and two decisions I'd rather you made.

    What I'd add: the optional userinfo endpoint — a GET taking a bearer token issued with profile scope, returning the same object as the profile property in the token response, discovered through a userinfo_endpoint key in the metadata document. Following the existing pattern in endpoint-auth that's ${mountPath}/userinfo — /auth/userinfo by default, alongside /auth/token and /auth/introspect — plus one line in the metadata controller. The spec mandates no particular path, so if you'd rather it sat elsewhere that's a one-line change.

    Why it pairs well with discovery, beyond completeness: because the profile is discovered from the user's h-card rather than configured, it can legitimately be absent at authorization time and appear later — someone adds an h-card to their homepage a week after signing in. With profile-in-the-token-response alone, a client is stuck with what it first got until the user re-authorizes. A userinfo endpoint lets it re-read. Less two features than the second making the first robust.

    Two things I'd rather you decided:

    1. Fresh or captured? Discovery runs on demand, so userinfo can re-fetch the h-card and return current data — which seems like the point of a separate endpoint. The alternative is returning whatever was captured when the token was issued. The spec says "identical to the profile property" without settling which, and the difference is user-visible: change your photo, and one updates while the other doesn't.

    2. Same PR or a follow-up? The discovery work is four files; userinfo adds a controller, a route, a metadata key and tests. I'd lean toward one PR since they share getProfileInformation and reviewing them together shows the whole feature — happy to split if you'd rather keep the diff small.

    One thing I'd leave out: the spec allows profile and/or email scope on the token. email stays unsupported as agreed, so userinfo returns the profile object only — flagging it so it doesn't read as an oversight.

    On errors, I'd return the profile object for a valid token carrying profile, and an OAuth-style error for one that doesn't, rather than an empty object — so a client can distinguish "not granted" from "no h-card found". Say if you'd rather it degrade silently, as the token response does.

  5. rmdes commented on Sep 5, 2026

    @rmdes
    CollaboratorAuthor

    Going ahead with the defaults unless you object: /userinfo re-discovers the h-card on each request, since a client comes to it precisely to read current data; and one PR, since the endpoint and the token response share getProfileInformation. Error for a token without profile scope, profile object otherwise, email untouched. Configuration first, discovery as fallback, as agreed above.

  6. paulrobertlloyd commented on Sep 5, 2026

    @paulrobertlloyd
    Collaborator

    Going ahead with the defaults unless you object

    Sounds good!

  7. added a commit that references this issue on Sep 13, 2026
    0751857
  8. added a commit that references this issue on Oct 7, 2026
    7a27fa6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions