Hi, I'm Aaron Parecki, and I'm super excited to be here today and talk to you about how
ActivityPub can take advantage of some of the new features coming in OAuth 2.1.
I wear a lot of hats, so let me give you a little background on where I'm coming from.
I work at Okta as a senior security architect, where I teach people about OAuth and help
people understand standards and build secure software.
I'm also the co-founder of IndieWebCamp where we help people own their data and have a
website and have a presence online.
I also work at the OAuth Working Group at the IETF actually developing the OAuth
standards themselves, and I'm involved in several of the efforts going on there right
now. So a quick outline of the agenda.
We're first going to talk about what OAuth is, what problems it solves, talk about what's
coming in OAuth 2.1, talk about how ActivityPub can take advantage of OAuth 2.1.
We'll also talk about what's missing from OAuth 2.1, that ActivityPub needs in order to
actually use it successfully.
And I'll give a sneak peek of some of the future work coming down the pipeline from the
group as well. So first, a little background on OAuth.
Before we had OAuth, we would see this kind of prompt in applications all the time where
the application would actually have to collect your username and password, send it to the
service and somehow use it to log in as you.
This has a number of drawbacks.
And it's most obvious in the scenario where we're using third party apps that aren't
built by the provider of the service you're logging in to.
So in the case of ActivityPub specifically, any time you're using a third party app to
talk to your own ActivityPub server, what would happen, for example, if you decided you
no longer like that app or no longer trust the app developer and you want to revoke the
app's access? The only real way to do that is to change your password because you can't
actually trust the developer hasn't stored that password.
Speaking of trust, do you trust them at all to actually not storing your password and
really just use it in that initial login?
You have no guarantees and you don't know what their application is doing, even though it
might be open source.
You never really sure because you probably downloaded the binary from an app store.
Another aspect of giving your password to an app developer is they can actually do
anything that you can do.
There's nothing really stopping them from taking that password and logging in as you and
then doing things like changing your password or deleting your account.
Once you give your password to an application, that's it.
That's the end. They can do whatever they want.
You really should not be giving your password to your account to anybody except for that
account came from. So how does OAuth solve this?
Well, OAuth steps in and changes the flow around so that you never actually have to give
your password to the application.
So without OAuth, you are stuck in this kind of situation where you have to first give
your password to the application.
It then would go and talk to the API of your service to interact with that service and
act as you within the network, creating posts, showing your timeline, whatever that is.
Now that's got all these problems of you've just entrusted this application with all of
your data and the ability to change your account.
So OAuth does is it changes things around.
And first you actually go and talk to your own service.
You get back this sort of key like thing, you give that to the application and it can use
that to go and access your account.
The benefit here is that that is not your password.
That thing that the application is using can be revoked.
It can be changed later.
And also importantly, it can be limited in what it can do.
It doesn't have to have full access to that data.
I like to think of this analogy of checking into a hotel where you go to the front desk
of a hotel, you show that person your ID and your credit card.
They give you back a hotel key.
Now, when you go to the door of your room, you swipe the hotel key on the door and the
door opens up and lets you in.
You don't have to give the door your ID card.
The nice thing about this situation is this hotel key can give you access to one or more
doors in the hotel. It can give you access to the pool or the gym.
And when your stay is over, that key card will be revoked and will no longer open these
doors. So in OAuth terms, the person at the front desk is the authorization server.
The key card is an access token, and the door is the API or the resource server.
So that's a quick overview of what OAuth is.
Now, let's talk about why ActivityPub specifically needs OAuth.
The specific problem that OAuth is solving here is the ability for a client application
to talk to a server to create posts or show the timeline.
Ideally, you don't want to have to give that application your password.
You would instead let it get an OAuth access token, which you can then use at that API.
And that's because you're going to be browsing the app stores or compiling open source
software to use one of these applications.
You may not trust all of the app developers to do the right thing if they had to actually
handle your password.
So that's all well and good.
But let's talk about some of the reasons that OAuth has been sort of awkward in the past
and what we need to fix in order to actually make it useful in this situation.
One of the main ways you'll see OAuth deployed today is that before an application can
even begin to request an access token, the developer of the application has to actually
go register for API keys themselves.
So the developer will go to the Twitter website or Google's developer console or whatever
the server is, go register for API keys, put that into their application.
So at that point, the application can actually launch a browser open to the OAuth service
where the user logs in. They're not giving their password to the application.
That OAuth server then redirects them back to the application, issuing them a temporary
code which the application can exchange for an accesss token.
Now, that's, of course, a very high level summary, if you are curious about the details of
how this works. Check out OAuth.net.
There's lots of good resources linked from there to learn more about this.
So what makes us a little bit awkward for ActivityPub?
Well, again, before the flow can even start, traditionally the developer has to go
register for API keys at the OAuth server.
Now in ActivityPub, there are potentially an infinite number of OAuth servers.
Right? Everybody's personal instance would be an OAuth server.
So there isn't a single place the developer could go to register for API keys.
So we need this to work in a way that doesn't require registering for API keys.
The other problem here is that in the case of mobile apps or single-page apps, there
isn't actually a way to safely deploy a client secret or any sort of API key in those
applications because it just wouldn't be secret for very long.
So we have to also be able to do an OAuth flow in a way that doesn't require a client
secret. One of the other things missing is this aspect of discovery -- of OAuth server
discovery. How does the application actually know which OAuth server to redirect the user
to to go log in?
Typically, the way OAuth discovery works today is the application is actually configured
with a short list of OAuth servers, maybe just one even.
And the discovery document that's talked about in the OAuth specs is about discovering
the capabilities of the server rather than necessarily discovering the OAuth server given
a user's identity.
One of the other problems of OAuth 2 is that the spec itself is actually pretty
underspecified and leaves a lot of decisions up to implementors of OAuth servers.
And we don't really want to have to make every ActivityPub server developer go through
all this process of making all those decisions themselves.
So if you go try to read the specs and actually try to see what's out there, you're going
to find that it looks like a maze.
It is a minefield full of different RFCs, some of them are created in different working
groups. There's extensions, there's extensions to extensions, there's best current
practices. And it can be a kind of overwhelming.
So really, one of my main goals with creating OAuth 2.1 is to actually consolidate this
and simplify it into a way that is much easier to understand and easier to get started.
So OAuth 2.1 Specifically is an attempt to clean up OAuth 2 and consolidate it and talk
about just the best practices of specs.
We're rolling up the most commonly deployed features and the most secure practices into a
new document called OAuth 2.1.
We're also removing some of the deprecated features of OAuth 2.0 that have proven to be
insecure and nobody really uses anymore.
So if you look at the state of art, of what OAuth 2 actually says, including all of the
best practices and documents that have been published in the last 10 years, it looks kind
of like this. It's a pile of specs, it's a patchwork.
And if you actually look into it, actually read through all of this, it's actually a much
simpler picture. At the end, it actually looks a lot more like this.
So this is really what OAuth 2.1 Is meant to do.
It's meant to consolidate and show you a more limited set of options that the OAuth group
believes is the most secure way to deploy this.
All right. So let's get into the details of how ActivityPub can take advantage of OAuth
2.1. First of all, one of the things OAuth 2.1 does is it requires PKCE for all OAuth
clients. Now, PKCE is a mechanism that was created for mobile apps, but it turns out it
has several security properties that make it useful for every kind of application.
So it's actually just a recommendation to always use it.
This also happens to solve the problem of mobile applications and single page
applications not being able to use a client secret safely.
I want to talk a little bit about PKCE to give you an idea of how it works.
We're going to go through an example of a PKCE flow, just sort of at a high level.
Again, if you're curious about the details on the wire, what this looks like, check out
OAuth.net. There's a lot of links and resources there.
All right. We're going to walk through this step by step, starting with the user who has
just downloaded an application and is ready to go log in.
So they download the app, they click the button that says log in.
The application says, all right, well, don't give me your password.
I can't do anything with your password.
Instead, I'm going to generate a new secret right now.
And this secret is going to be for this particular instance, for this request.
And it's going to only live on the device.
It then calculates a hash of the secret using SHA256.
Now, of course, the idea with the hash is that it's a one way operation.
So if I told you to pick ten random numbers and then add them together and tell me the
sum, I would not be able to tell you which ten numbers you chose.
Now, that's obviously not the best hashing algorithm, but that's the general idea.
SHA256 is like a much fancier version of that.
Okay, so we've got the hash calculated.
Now, this is actually included in a URL that the application builds to go direct the user
to their OAuth server.
So the application builds this URL, opens a browser and that URL says here's the
application that's trying to log in.
Here's the scope of this request.
It's trying to get full access to the account or only read only access.
And here's also that hash.
So the user lands at the OAuth server, the OAuth server then gets the user to log in
there, and then shows in this prompt.
Do you actually want to allow this application to do these things?
It's asking. And if the user clicks, yes, the server will generate a temporary one-time
-use code, sending it back into the user's browser to deliver back to the application.
So at this point, this is all been sent through the address bar, which is the sort of
untrusted environment where we're never really sure if it worked, can never really be
sure that everything that's gone through the address bar hasn't been snooped on or copied
or stolen by somebody, but that's fine because the secret has never actually left the
device yet. So now the application can go make a direct POST request to the server using
that code it got from the address bar.
And it has to prove that it actually initiated the request for that authorization code by
sending the secret it generated in that first step.
That way, even if someone did steal the code from that redirect, it's not actually useful
because they couldn't have also stolen the secret.
So at this point, the server says, cool.
Well, when I generated that code, I first of all, see that it hasn't been used yet.
It was generated within a couple of seconds ago.
And I saw this hash.
So let me calculate the hash of this secret that's in this request.
Compare them. If they match, then the server knows the thing making this request is the
same thing that started the flow so then it can issue the access token in the response
and now the application can go make requests with the access token.
So that's the basics of the PKCE mechanism.
The steps highlighted in bold here are specifically the things that are documented in the
PKCE extension to OAuth that is being consolidated and brought into OAuth 2.1.
So this is all well and good, but what's missing?
We actually left out a few important steps in order to make this work for ActivityPub.
Specifically, how do we discover the OAuth server that we're actually going to go and
start this flow with? Also, OAuth doesn't actually talk about user identity.
OAuth is just a way to get an access token.
Figuring out who the user is is left up to the specific server or other extensions like
OpenID Connect. One of the other things that ActivityPub needs to do is actually define a
list of scopes, things like the ability to make posts or the ability to read your
timeline, the ability to follow people, unfollow people, delete posts, any action that
you could do with your ActivityPub server can be its own scope.
So we need a list of those scopes as well.
So let's talk through a couple of these, starting with discovery.
So one of the primary ways that discovery works on the web today is there's just a
handful of OAuth providers and the website will put links to all of them.
That's where you'll see these links, like sign in with GitHub, sign in with Google, sign
in with Twitter. Now, that's all well and good for when there's only a handful of OAuth
servers. But in the ActivityPub world, there isn't a defined list of these because any
person can set up their own ActivityPub server.
So it doesn't really make sense to have a "Sign in with Mastodon" button because there
isn't just one Mastodon instance.
So what we actually need is for the user to tell the application which server they use,
and that's the idea of bring your own identity into the system.
So the first thing the application needs to do is actually ask the user, what is your
website, what is your server?
And this can actually work well, even for a single user or multi-user domains.
So for the single user case, I would enter my website aaronparecki.com.
The application can then go discover where my OAuth server is and it might be an endpoint
on my domain if I'm self-hosting my own OAuth server.
Now this also works just as well for multiuser websites.
If you ask the user to enter their server name, they might enter mastodon.social and then
they may have an account at that server.
Now, in this case, multiple users on the server would have the same OAuth server, which
again might also be on that same server.
One of the other neat things, though, is that this can actually work with multiuser
domains where the server is different for each user at that domain.
And that might be for things like using shared hosting or like a tilde.club website where
each user can actually point to their own OAuth server.
And the application would then discover each user's OAuth server independently.
Okay, so let's look at how this works step by step.
The first thing the application has to do is put up a prompt asking the user to enter
their server name. At that point, the application can then make a HEAD or a GET request
to look for the links to the OAuth server.
Once it finds the user's authorization endpoint, then the application can start the
request by building up a URL from that authorization endpoint.
So that's going to do things like include the client ID of the application, includes the
scopes it's requesting, include the PKCE parameters, build that URL and open up a browser
to that URL. This is what the complete authorization request looks like.
This is an OAuth request with the PKCE extension.
So it's got the response_type=code saying we're doing the authorization code flow, it's
got the client_id, redirect_url where the application is ready to handle the response
from the OAuth server. It has scope, which again, we'll need to define a list of scopes
that are consistent across all ActivityPub clients and servers.
State is just a random string it makes up.
And then that code_challenge is the hash of the PKCE secret it generated.
So this point, the application is going to open up a browser, and on a mobile app, for
example, it might look like this where it pops up sort of in front of the app, that URL
at the address bar is that OAuth server URL.
The user logs in here where the app can't actually steal the user's password from this
dialog. And if the user approves the request after they see what the app is trying to do,
then the server will generate that temporary code and send the redirect, which then will
close that dialog.
And the app can then exchange that code for an access token.
And then again, because we're doing PKCE here, the application actually needs to include
its own PKCE code_verifier, which is that secret it makes up at the beginning in order to
actually exchange that code, and that protects that code from being used if it's stolen.
So here's the final POST request the app makes for the access token.
So we've got grant_type=authorization_code, the actual code it gets from the query string,
client_id, redirect_url, and then the PKCE code verifier.
And you might notice there's no client secret here because we can't really deploy these
apps with a client secret anyway.
The response it gets from this will be the access token.
Now, there's also an interesting opportunity here to extend OAuth to include a little bit
of user identity, since every application in ActivityPub is going to need to know the
user's identity anyway in order to do things like configure the interface with their
profile photo or their name.
Let's shortcut the step of having the app have to go and make another request with the
access token by just returning profile info in this response itself.
So here we can extend the OAuth access token response to include user profile information,
just the bare minimum the app needs in order to get started.
The user's name, URL, and profile photo.
So this is an example of a complete OAuth exchange using PKCE with no registration needed
for applications. What I just showed you is actually documented as an extension to OAuth
already. This is called IndieAuth.
So IndieAuth takes OAuth as a foundation and adds a few things into it.
One of the first requirements is that client IDs are actually URLs.
So there's a couple of reasons for this.
We already know that we can't deploy clients with a client secret, so we don't really
need the client secret. Once we add PKCE, the client secret doesn't really add any
additional security either.
So we might as well drop the idea of using client secrets completely.
So the client ID in OAuth is actually supposed to identify the application within the
system. And the way it does that traditionally is by having a developer register that
information in the developer website.
So if you think about the purpose of the client ID, it is to identify the app.
Well, turns out most apps have a website.
The user is going to have to download the app from somewhere.
And as an app developer, you probably want to have a website for your app so that your
users can find out more about it.
So if that website becomes the client ID as a URL, then we already have this global app
registration database, which is the whole DNS system.
So we can just use that as a client ID because we don't need the client ID and client
secret to act as authentication.
This also gives the OAuth server the ability to discover client information from that
website looking up its logo or name from that URL.
The next thing IndieAuth says is that user IDs are URLs, which again in this kind of
system actually makes a ton of sense.
Every ActivityPub actor already has a URL.
It may not be something that the user is familiar with and necessarily sees, but people
do understand they have profile pages at their server and that just happens to be a
global registry, again, thanks to DNS.
So if user IDs are URLs, we can then also discover the user's OAuth server from their URL
or at the very least from their server name.
So again, all this stuff is documented at IndieAuth.net.
Go take a look there, there's links to some blog posts and other presentations, and the
actual spec itself, where you can learn more about how this all works.
Now, I do want to point out this has nothing to do with server to server federation of
sending activities between different servers.
That's a whole different problem.
This is specifically solving the problem of how a client application can authenticate to
ActivityPub servers.
Okay, so that's the basics of OAuth 2.1, plus the IndieAuth extension that talks about
how to use it in this sort of Open Web environment.
Now, I want to give you a little sneak peek of some of the new work coming out of the
OAuth group that I think might be relevant in the future to this kind of environment as
well. I want to caveat this by saying these things are still in progress in the working
group itself. These are not final specifications.
They're actively being worked on.
But I do think it's useful to get a sort of sneak peek and keep an eye on how this stuff
is evolving. And also just to say, if this is interesting to you, this is a good time to
get involved. The OAuth working group is open to anybody, and you are welcome to just
come and join the conversation.
Okay, so a couple of the extensions that are being worked on right now that I think would
be useful: Rich Authorization Requests and Pushed Authorization Requests.
We're going to talk about these one by one.
So Rich Authorization Requests is the idea that sometimes the OAuth idea of scope is too
limiting. Now, it actually works pretty well for most things.
Like we can say the application needs the right scope in order to publish posts or the
application needs the follow a scope to be able to follow users.
And you can get pretty far by defining these strings to mean these specific actions.
Where this idea falls down is where you want to authorize specific transactions, like,
for example, following a specific person or posting a specific post, or moving money
around, right, where you might want to authorize the ability to pay somebody this amount
from this bank account.
That's all now to fine-grained to represent with these sort of coarse scopes.
So we need some sort of new pattern to represent these sorts of rich authorizations.
So that's where this spec comes in.
It defines a JSON format for actually representing complex authorizations that are more
specific than can be represented by scopes.
So it defines a general structure and extension points so that systems can define their
own domain specific things within the structure.
So here's an example from the spec talking about the banking industry use of this spec.
Now, you can imagine that if you tried to put this into the URL of the initial OAuth
request, it's going to fall apart real quick because this is going to get very long.
And that's where this other spec comes into play, Pushed Authorization Requests.
So backing up a minute -- when we started the OAuth flow, we usually start by building a
URL and then opening the browser to that URL.
When we build that URL we include things about the request the app is making.
Things like the app's client ID, the redirect URL, the scope, and then of course like
state or the PKCE parameters.
The problem with sending that in the URL is that the user or anybody has the ability to
change that before it actually hits the OAuth server.
So if you try to log in to any OAuth application today and try to grab that first URL it
makes, see what happens if you just sort of change parameters around.
You might get some errors, you might find that unexpected things happen.
So if we tried to add the rich authorization into that URL, first of all, it's going to
get very long. Also, the encoding is going to really complicated, and there's potentially
sensitive information in that request as well.
Once we start talking about bank accounts or a specific user accounts.
So the idea here with Pushed Authorization Requests is that instead of the app building a
URL to start the flow, the app actually first makes a POST request with the complete
details of the request up to the server, and then it gets back a short token that
represents what it just sent.
Then it can launch the browser to a URL with that short representation of the request in
it, which also does not actually contain any data so the user can't modify it.
So that solves the problem of needing to pack in more information than can fit into a URL
into this first request.
It also makes it more secure because the application may be requesting access to things
that are sensitive, like usernames or bank account info.
Okay, so those are just two of the extensions I wanted to talk about.
Again, these are still in progress.
They're not done. But if you're interested in these, this is a good time to take a look
at them. The other thing I want to mention is this effort you may have heard about that
is tentatively called OAuth 3.
This work is being done in a new group at the IETF called GNAP, no relation to GNU.
It's also been codenamed TxAuth.
But the names are kind of in flux right now.
This is actually a complete reimagining of the protocol.
So just like OAuth 2 changed almost everything compared to OAuth 1, this effort is
changing almost everything compared to OAuth 2.
It'll work in a completely different way.
It'll have many different security properties and actually will be much more flexible in
terms of different ways it can be deployed.
This is even less far along than OAuth 2.1 or any of these other extensions we just talked
about. But it's good to sort of keep that on the radar and see how it works.
The other reason I want to bring it up here is because, again, this kind of work benefits
from more people being involved in it from more different kinds of backgrounds.
And this sort of open web and federated space really should have more influence on the
specs being developed by the IETF that are being used industry wide.
We want to make sure that these specs can also cover these federated use cases and aren't
just limited to the enterprise space.
So that's it for today.
Thank you so much for watching.
I hope you learned something, I hope you got some new ideas, and I encourage you to try
this stuff out and experiment.
I'll leave you with a few links where you can go to learn more about this work.
You can follow me at aaronparecki.com.
My ActivityPub handle is @aaronpk@aaronparecki.com, you can find more information about
IndieAuth at IndieAuth.net That talks about the extension and what it builds on to OAuth.
You can also learn more about OAuth itself at OAuth.wtf, which is also where I have cat
stickers that you can get.
Thank you all so much for watching and have a great rest of the day.