Thoughts on IndieAuth Profile URLs
For Authorio 0.8.3, I made a change to the way it handles user profile URLs, and that forced me to think a little more on exactly what a profile URL is.
As I’ve developed Authorio, I’ve tried to keep the underlying schema flexible enough that it can handle multiple users authenticating on the same site. This is something that is explicitly mentioned in the spec, although I think the main use-case is one user per site (each person authenticating on their own personal web site).
Developing for multi-user
One rough edge in the Authorio code had to do with how it handled user profile URLs. There needs to be some way to map a URL to a User model, but user profile URLs start with a hostname and models are completely agnostic as to HTTP details like hosts. I rejected simply storing a full URL as an attribute of the model, because that meant doing some sort of database migration of the site moved to a differnt host. The hostname is properly resolved only at the time of the HTTP request, and that meant I had to write some awkward code in the controller that assembled the URL from the hostname and attributes on the user model.
But then a thought occurred to me: why not simply use Rails’ built-in routing mechanism. If I wrote a route
for user#show
then I could simply call url_for @user
and get a unique URL. It would look something like
https://example.com/authorio/user/1
.
This is very different from the single-user case, where the user profile URL is simply the root of the host, ie,
http://example.com
. But the spec states
that it’s explicitly allowed for an endpoint to return a different URL that what the user originally input
into the client’s auth form (as long as it can verify it’s the same server).
The Problem with Profile URLs
When I tried to use my new Authorio endpoint to login to the IndieWeb wiki, it came up in a way that disappointed me.
Previously, when I had logged in as a single-user, I was recognized as Reiterate.app
(the web site). But now, I showed
up as Reiterate.app authorio user 1
which is kind of ugly. That URL wasn’t my identity, it was just something
that my endpoint had exchanged to verify me. And that’s the problem.
Profile URLs are identifiers, but they aren’t user-facing identifiers.
When you have an account on a website, that website tracks your identity through some backend mechansim. You might be
User 234
or perhaps some UUID. You might be tracked by your username, but that would be a poor design. Your username
is an attribute on your user account, and it’s the user-facing aspect of your identity.
Here is a concrete example. On Discord my username would be Rattroupe
, and I think of myself as “Rattroupe on Discord”.
That’s my identity. If I want someone to message me, I can tell them to message Rattroupe on Discord. (Side note: Discord actually appends
a hashtag-with-number to each username to make it unique. It’s only necessary to use that bit if the context is ambiguous).
On the backend, Discord tracks users via their snowflake id
, which is a big integer with some uniqueness guarantees. My
user account would look something like this:
{
"id": "80351110224678912",
"username": "Rattroupe",
"discriminator": "1337",
"avatar": "8342729096ea3675442027381ff50dfe",
"verified": true,
"email": "rattroute@reiterate.app",
"flags": 64,
"banner": "06c16474723fe537c283b8efa61a30c8",
"accent_color": 16711680,
"premium_type": 1,
"public_flags": 64
}
On Discord, rattroupe
is my user-facing identifier, and 80351110224678912
is my backend-facing identifier. They’re both
useful, depending on the context. The user-facing identifier is the one that I think of myself as, and what I trade with
other users. The backend-facing identifier is used if I want to interface with the API.
IndieAuth conflates these two use cases, and that leads to confusion. For people who own their own web site, and use it as a single-user authentication endpoint, then their user id is just the URL of that site, and that’s fine. But that doesn’t work for multi-user sites, where people end up with awkward identifiers that are hard to type and exchange.
This confusion can be seen in how the profile URL is used, and also in the initial authentication workflow. When a user wants to log in via IndieAuth, the standard workflow is:
- On the client, the user enters a URL, which is supposedly their profile URL. It’s what the user thinks of as their login.
- The client then “normalizes” this URL (by adding a scheme, or a trailing slash)
- The client uses that URL to discover the authentication endpoint and redirects the user there
- The user authenticates with the endpoint
- The endpoint then returns a (potentially completely different) profile URL back to the client.
This seems unnecessarily awkward, and it’s due to the fact that the “profile URL” is being used in different ways.
History and Evolution
It feels like IndieAuth is being pulled in two different directions. On the one side are the single-user sites. This is where IndieAuth originally came from and where it has the most support. IndieAuth grew up with an IndieWeb ethos of “You are Your Own Web Site,” encouraging people to make their own sites and fostering the approach to enable people to own their own content. That’s fine, and I agree with the sentiment, but it leads people down the path of “I am this particular hostname”
On the other side are the multi-user sites. For these users, identity is not just a hostname but a full URL. Or maybe
it’s some better user-facing identifier. IndieAuth has been slowly evolving towards this direction, with the latest movement
being the profile
scope which returns greater user identifying information that just the me
URL. Unfortunately,
when I implemented this I discovered that
there is zero take-up. No IndieAuth clients actually use this information.
Authorio finds itself squarely between these two camps. As a Rails plugin, it’s useful for anyone who wants to use Rails to build their own website. Rails is one of the most popular web frameworks and it gained its popularity from the ease in which it allows people to quicky set up a site. If you want to make a personal single-user website Rails is a grweat way to go.
But Rails brings a lot of power with it. You get a whole database backend, and adding multiple users is easy as well. Rails has a lot of solutions for bulding multi-user capability into a site, and therefore I felt Authorio had to support this model.
IndieAuth has been slowly adjusting its authentication workflow as well. The “me” parameter, which is what the user entered at the client as their profile URL, is being deprecated since “most implementations ignored it anyways”.
What I Would Like to See
I wish that IndieAuth made explicit the current schism between user-facing and backend identifiers. There have been some initial moves in this direction but clarifying this in the spec could increase adoption.
The spec could say something along the lines of “The User Profile URL SHOULD NOT be displayed to the user, or used in an interface that identifies a user.” That would be fairly radical, and would alienate the people who identify with a hostname.
Perhaps the authentication response could return a username
attribute along with the profile URL. This attribute then
should be used by the client to identify the user in preference to the URL. This would allow endpoints to create whatever
user-facing identifier they wished, and properly hide a complex profile URL. Old-style single-user endpoints which identify
only by a hostname would continue to function as before.
In the meantime, I’ve added a config option to Authorio to explicitly set it to single-user mode (where profile URLs are just the hostname) and multi-user (which expands the profile URL as described above).
For the login workflow, let’s make explicit the fact that what the user enters is not a profile URL. The first input from the user, at the client site, should be a hostname. It’s not a URL, it’s just the name of the host that the user is using for authentication. So the authentication flow is more like:
- At the client, the user enters the host of their authenticating server.
- The client doesn’t need to normalize this at all. It just looks up the host and discovers the authentication endpoint.
- The user authenticates with the endpoint
- The endpoint returns a username and profile URL, neither of which are necessarily related to the initial hostname.
This seems much cleaner to me. It’s not a huge change from what exists currently, it’s just a slight reframing of what a profile URL means and what it shoud be used for.